前言
- 在项目中使用
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}` }
}
- 装饰器函数
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;
}
}
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()
@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状态的展示;当有错误时,全局可对错误进行处理(错误上报等)。 - 合理利用装饰器可以极大的提高开发效率,对一些非逻辑相关的代码进行封装提炼能够帮助我们快速完成重复性的工作,节省时间。