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
相關文章
相關標籤/搜索