React專案實現全域性 loading 以及錯誤提示

前端小黑發表於2019-09-29

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 寫起來十分繁瑣:

    • 如果要寫非同步方法並處理 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類,實現不同 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

相關文章