前言
- 在项目中使用
loading
,一般是在组件中用一个变量( 如isLoading
)来保存请求数据时的loading
状态,请求api
前将isLoading
值设置为true
,请求api
后再将isLoading
值设置为false
,从而对实现loading
状态的控制,如以下代码:
import { Spin, message } from 'antd';
import { Bind } from 'lodash-decorators';
import * as React from 'react';
import * as api from '../../services/api';
class HomePage extends React.Component {
state = {
isLoading: false,
homePageData: {},
};
async componentDidMount () {
try {
this.setState({ isLoading: true }, async () => {
await this.loadDate();
});
} catch (e) {
message.error(`获取数据失败`);
}
}
@Bind()
async loadDate () {
const homePageData = await api.getHomeData();
this.setState({
homePageData,
isLoading: false,
});
}
render () {
const { isLoading } = this.state;
return (
<Spin spinning={isLoading}>
<div>hello world</div>
</Spin>
);
}
}
export default HomePage;
- 然而,对于一个大型项目,如果每请求一个
api
都要写以上类似的代码,显然会使得项目中重复代码过多,不利于项目的维护。因此,下文将介绍全局存储loading
状态的解决方案。
思路
- 封装
fetch
请求(传送门?:react + typescript 项目的定制化过程)及相关数据请求相关的api
- 使用
mobx
做状态管理 - 使用装饰器
@initLoading
来实现loading
状态的变更和存储
知识储备
- 本节介绍与之后小节代码实现部分相关的基础知识,如已掌握,可直接跳过???。
@Decorator
- 装饰器(Decorator)主要作用是给一个已有的方法或类扩展一些新的行为,而不是去直接修改方法或类本身,可以简单地理解为是非侵入式的行为修改。
- 装饰器不仅可以修饰类,还可以修饰类的属性(本文思路)。如下面代码中,装饰器
readonly
用来装饰类的name
方法。
class Person {
@readonly
name() { return `${this.first} ${this.last}` }
}
Js中文网 – 前端进阶资源教程 www.javascriptC.com,typescript 中文文档
一个帮助开发者成长的社区,你想要的,在这里都能找到
- 装饰器函数
readonly
一共可以接受三个参数:- 第一个参数
target
是类的原型对象,在这个例子中是Person.prototype
,装饰器的本意是要“装饰”类的实例,但是这个时候实例还没生成,所以只能去装饰原型(这不同于类的装饰,那种情况时target
参数指的是类本身) - 第二个参数
name
是所要装饰的属性名 - 第三个参数
descriptor
是该属性的描述对象
- 第一个参数
function readonly(target, name, descriptor){
// descriptor对象原来的值如下
// {
// value: specifiedFunction,
// enumerable: false,
// configurable: true,
// writable: true
// };
descriptor.writable = false;
return descriptor;
}
readonly(Person.prototype, 'name', descriptor);
// 类似于
Object.defineProperty(Person.prototype, 'name', descriptor);
- 上面代码说明,装饰器函数
readonly
会修改属性的描述对象(descriptor
),然后被修改的描述对象再用来定义属性。 - 下面的
@log
装饰器,可以起到输出日志的作用:
class Math {
@log
add(a, b) {
return a + b;,JS中文网 - 前端进阶资源教程 www.javascriptc.com
}
}
function log(target, name, descriptor) {
var oldValue = descriptor.value;
descriptor.value = function() {
console.log(`Calling ${name} with`, arguments);
return oldValue.apply(this, arguments);
};
return descriptor;
}
const math = new Math();
// passed parameters should get logged now
math.add(2, 4);
- 上面代码说明,装饰器
@log
的作用就是在执行原始的操作之前,执行一次console.log
,从而达到输出日志的目的。
mobx
- 项目中的状态管理不是使用
redux
而是使用mobx
,原因是redux
写起来十分繁琐:- 如果要写异步方法并处理
side-effects
,要用redux-saga
或者redux-thunk
来做异步业务逻辑的处理 - 如果为了提高性能,要引入
immutable
相关的库保证store
的性能,用reselect
来做缓存机制
- 如果要写异步方法并处理
redux
的替代品是mobx
,官方文档给出了最佳实践,即用一个RootStore
关联所有的Store
,解决了跨Store
调用的问题,同时能对多个模块的数据源进行缓存。-
在项目的
stores
目录下存放的index.ts
代码如下:
import MemberStore from './member';
import ProjectStore from './project';
import RouterStore from './router';
import UserStore from './user';
class RootStore {
Router: RouterStore;
User: UserStore;
Project: ProjectStore;
Member: MemberStore;
constructor () {
this.Router = new RouterStore(this);
this.User = new UserStore(this);
this.Project = new ProjectStore(this, 'project_cache');
this.Member = new MemberStore(this);
}
}
export default RootStore;
- 关于
mobx
的用法可具体查看文档 ?mobx 中文文档,这里不展开介绍。
代码实现
- 前面提到的对
loading
状态控制的相关代码与组件本身的交互逻辑并无关系,如果还有更多类似的操作需要添加重复的代码,这样显然是低效的,维护成本太高。 - 因此,本文将基于装饰器可以修饰类的属性这个思路创建一个
initLoading
装饰器,用于包装需要对loading
状态进行保存和变更的类方法。 - 核心思想是使用
store
控制和存储loading
状态,具体地:- 建立一个
BasicStore
类,在里面写initLoading
装饰器 - 需要使用全局
loading
状态的不同模块的Store
需要继承BasicStore
类,实现不同Store
间loading
状态的“隔离”处理 - 使用
@initLoading
装饰器包装需要对loading
状态进行保存和变更的不同模块Store
中的方法 - 组件获取
Store
存储的全局loading
状态
- 建立一个
- Tips:?的具体过程结合?的代码理解效果更佳。
@initLoading 装饰器的实现
- 在项目的
stores
目录下新建basic.ts
文件,内容如下:
import { action, observable } from 'mobx';
export interface IInitLoadingPropertyDescriptor extends PropertyDescriptor {
changeLoadingStatus: (loadingType: string, type: boolean) => void;
}
export default class BasicStore {
@observable storeLoading: any = observable.map({});
@action
changeLoadingStatus (loadingType: string, type: boolean): void {
this.storeLoading.set(loadingType, type);
}
}
// 暴露 initLoading 方法
export function initLoading (): any {
return function (
target: any,
propertyKey: string,
descriptor: IInitLoadingPropertyDescriptor,
): any {
const oldValue = descriptor.value;
descriptor.value = async function (...args: any[]): Promise<any> {
let res: any;
this.changeLoadingStatus(propertyKey, true); // 请求前设置loading为true
try {
res = await oldValue.apply(this, args);
} catch (error) {
// 做一些错误上报之类的处理
throw error;
} finally {
this.changeLoadingStatus(propertyKey, false); // 请求完成后设置loading为false
}
return res;
};
return descriptor;
};
}
- 从上面代码可以看到,
@initLoading
装饰器的作用是将包装方法的属性名propertyKey
存放在被监测数据storeLoading
中,请求前设置被包装方法的包装方法loading
为true
,请求成功/错误时设置被包装方法的包装方法loading
为false
。
Store 继承 BasicStore
- 以
ProjectStore
为例,如果该模块中有一个loadProjectList
方法用于拉取项目列表数据,并且该方法需要使用loading
,则项目的stores
目录下的project.ts
文件的内容如下:
import { action, observable } from 'mobx';
import * as api from '../services/api';
import BasicStore, { initLoading } from './basic';
export default class ProjectStore extends BasicStore {
@observable projectList: string[] = [];
@initLoading(),JS中文网 - 前端进阶资源教程 www.javascriptc.com
@action
async loadProjectList () {
const res = await api.searchProjectList(); // 拉取 projectList 的 api
runInAction(() => {
this.projectList = res.data;
});
}
}
组件中使用
- 假设对
HomePage
组件增加数据加载时的loading
状态显示:
import { Spin } from 'antd';
import { inject, observer } from 'mobx-react';
import * as React from 'react';
import * as api from '../../services/api';
@inject('store')
@observer
class HomePage extends React.Component {
render () {
const { projectList, storeLoading } = this.props.store.ProjectStore;
return (
<Spin spinning={storeLoading.get('loadProjectList')}>
{projectList.length &&
projectList.map((item: string) => {
<div key={item}>
{item}
</div>;
})}
</Spin>
);
}
}
export default HomePage;
- 上面代码用到了
mobx-react
的@inject
和@observer
装饰器来包装HomePage
组件,它们的作用是将HomePage
转变成响应式组件,并注入Provider
(入口文件中)提供的store
到该组件的props
中,因此可通过this.props.store
获取到不同Store
模块的数据。@observer
函数/装饰器可以用来将React
组件转变成响应式组件@inject
装饰器相当于Provider
的高阶组件,可以用来从React
的context
中挑选store
作为props
传递给目标组件
- 最终可通过
this.props.store.ProjectStore.storeLoading.get('loadProjectList')
来获取到ProjectStore
模块中存放的全局loading
状态。
总结
- 通过本文介绍的解决方案,有两个好处,请求期间能实现
loading
状态的展示;当有错误时,全局可对错误进行处理(错误上报等)。 - 合理利用装饰器可以极大的提高开发效率,对一些非逻辑相关的代码进行封装提炼能够帮助我们快速完成重复性的工作,节省时间。
参考资料
作者:前端小黑
链接:https://juejin.im/post/5d90341be51d45783a7729c5
看完两件小事
如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:
- 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
- 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程
本文著作权归作者所有,如若转载,请注明出处
转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com