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
状态的变动和存储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
,从而达到输出日志的目的。项目中的状态管理不是使用 redux
而是使用 mobx
,缘由是 redux
写起来十分繁琐:html
side-effects
,要用 redux-saga
或者 redux-thunk
来作异步业务逻辑的处理immutable
相关的库保证 store
的性能,用 reselect
来作缓存机制redux
的替代品是 mobx
,官方文档给出了最佳实践,即用一个 RootStore
关联全部的 Store
,解决了跨 Store
调用的问题,同时能对多个模块的数据源进行缓存。react
在项目的stores
目录下存放的 index.ts
代码以下:es6
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
状态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
。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
状态的展现;当有错误时,全局可对错误进行处理(错误上报等)。