React项目实现全局 loading 以及错误提示

前言

  • 在项目中使用 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 写起来十分繁琐: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类,实现不一样 Storeloading 状态的“隔离”处理
    • 使用 @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 中,请求前设置被包装方法的包装方法 loadingtrue,请求成功/错误时设置被包装方法的包装方法 loadingfalse

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 的高阶组件,能够用来从 Reactcontext中挑选 store 做为 props 传递给目标组件
  • 最终可经过 this.props.store.ProjectStore.storeLoading.get('loadProjectList') 来获取到 ProjectStore 模块中存放的全局 loading状态。

总结

  • 经过本文介绍的解决方案,有两个好处,请求期间能实现 loading 状态的展现;当有错误时,全局可对错误进行处理(错误上报等)。
  • 合理利用装饰器能够极大的提升开发效率,对一些非逻辑相关的代码进行封装提炼可以帮助咱们快速完成重复性的工做,节省时间。

参考资料

  1. ECMAScript 6 入门 | 装饰器
  2. Javascript 装饰器的妙用
  3. typescript | decorators
相关文章
相关标签/搜索