前言
- 在專案中使用
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
狀態的展示;當有錯誤時,全域性可對錯誤進行處理(錯誤上報等)。 - 合理利用裝飾器可以極大的提高開發效率,對一些非邏輯相關的程式碼進行封裝提煉能夠幫助我們快速完成重複性的工作,節省時間。