封裝 uniapp 請求庫的最佳實踐

零一行者發表於2024-07-12

背景

在前端開發中,HTTP 請求是與伺服器進行資料互動的核心手段。無論是獲取資料還是提交資料,前端應用幾乎都離不開 HTTP 請求。在 uniapp 中,uni.request 是官方提供的用於發起 HTTP 請求的基礎 API。然而,直接使用 uni.request 存在一些問題和不足,比如:

  1. 程式碼冗餘:每次發起請求時都需要編寫類似的配置程式碼,導致程式碼重複。
  2. 缺乏統一管理:沒有統一的地方管理請求引數、頭資訊、錯誤處理等,使得程式碼不易維護

意義

  • 簡化請求配置:在每次發起請求時,通常需要配置很多引數,比如 URL、請求頭、請求體等。透過封裝請求庫,可以設定預設的請求引數,簡化每次請求的配置操作,減少開發人員的工作量,提高開發效率。
  • 管理請求憑證:透過封裝請求庫,可以集中管理憑證,確保每次請求都自動攜帶正確的憑證。
  • 便於維護和擴充套件:封裝請求庫後,如果需要對請求邏輯進行修改或擴充套件,只需要在封裝庫中進行調整,而不需要在專案的各個地方逐一修改。此外,如果需要將請求庫更換為其他庫(例如 Axios),只需修改封裝的請求庫部分,而無需改動業務程式碼。
  • 提高使用者體驗:透過統一處理全域性請求 Loading 狀態,可以在請求進行中顯示載入提示,提升使用者體驗。

實現思路

1. 把 uni.request 改為支援 Promise 呼叫方式

uni.request 改為支援 Promise 呼叫方式的好處是可以避免回撥巢狀問題,並且可以藉助 async/await 實現同步呼叫。

實現方式大概有如下兩種:

1.1 透過 uni 自身提供的方法

呼叫 uni.request 時,如果不傳入 successfailcomplete 回撥函式,uni.request 的返回值將是一個 Promise 物件。

uni.request({
    url: "",
    // ... 其他配置
}).then(()=> {
}).catch(()=> {
}).finally(()=> {
});

1.2 透過 Promise 包裝

new Promise((resolve, reject)=> {
    uni.request({
        url: "",
        success(res) {
           resolve(res);
        },
        fail(error) {
           reject(error);
        },
        complete() {
        }
    });
});

具體採用哪種方式都可以,這裡選擇第一種。

2. 定義預設請求引數

在請求時,通常需要設定 content-typetimeout 等資訊。這些引數通常不會改變,因此可以設計為預設引數,同時保留外部覆蓋預設引數值的能力。

2.1 定義預設引數

// 定義預設引數
const defaultOptions = {
  timeout: 15000,
  dataType: "json",
  header: {
    "content-type": "application/json",
  }
};

2.2 合併外部引數與預設引數

提供外部覆蓋預設引數值的能力

const defaultConfig = {
  timeout: 15000,
  dataType: 'json',
  header: {
    'content-type': 'application/json',
  },
};
const wrapRequest = ({ 
    url = '', 
    data = {}, 
    method = "GET", 
    header = {} 
} = {}) => {
    return uni.request({
       ...defaultConfig,
       url,
       data,
       method,
       header: {
           ...defaultOptions.header,
           ...header
       }
    });
}

3. 統一處理請求憑證

在大多數系統中,介面請求通常需要傳遞使用者憑證。通常的做法是在請求的 Header 中新增 Authorization 屬性。為了簡化這個過程,可以透過攔截器來實現。

const TOKEN_KEY = 'token';

// 處理 token
const handleToken = (config) => {
  const token = uni.getStorageSync(TOKEN_KEY)
  if (token) {
    config.header.Authorization = token;
  }
}
uni.addInterceptor("request", {
  invoke: function (config) {
    handleToken(config);
  }
});

另外,系統通常會有多個環境。在這種情況下,可以根據不同的環境設定不同的 BASE_URL,這也可以透過攔截器來實現。

const BASE_URL = ""; 

const handleURL = (config) => {
  const { url } = config;
  if (!/https|http/.test(url)) {
    config.url = url.startsWith("/")
      ? `${BASE_URL}${url}`
      : `${BASE_URL}/${url}`;
  }
}

uni.addInterceptor("request", {
  invoke: function (config) {
    handleURL(config);
  }
});

如果有其他處理需求,可以直接在這裡新增。

4. 統一處理公共響應狀態碼

為了避免在多個地方處理公共的錯誤邏輯,例如憑證無效時跳轉到登入頁、移除本地 token 等,我們可以在全域性請求響應攔截器中集中處理這些問題。

const LOGIN_INVALID_CODE_LIST = ["INVALID_TOKEN", "EXPIRED_TOKEN"];
const SUCCESS = "SUCCESS";

uni.addInterceptor("request", {
  success(res){
    const { data: resData } = res;
    const { code, message } = resData;
    if (code !== SUCCESS) {
        // 如果響應程式碼在登入無效程式碼列表中
        if (LOGIN_INVALID_CODE_LIST.includes(code)) {
            uni.showToast({
                title: message,
                icon: "none",
            });
            uni.navigateTo({
                url: "/pages/login/login"
            });
            return;
        } else {
            // 處理其他錯誤程式碼
            return Promise.reject(resData)
        }
    }
    return Promise.resolve(resData)
   },
});

5. 封裝公共方法 GET、POST、DEL、PUT

為了進一步簡化請求引數,可以提供一系列方法,例如 GETPOSTDELETEPUT

export const get = (params) => wrapRequest({ ...params, method: 'GET' });
export const post = (params) => wrapRequest({ ...params, method: 'POST' });
export const put = (params) => wrapRequest({ ...params, method: 'PUT' });
export const del = (params) => wrapRequest({ ...params, method: 'DELETE' });

這樣做的好處,它消除了每次呼叫時顯式傳入 HTTP 方法的需要,使程式碼更簡潔、更易讀。這樣做的好處是你在呼叫這些方法時只需關注請求引數,而不需要重複指定 HTTP 方法。

6. 定義全域性請求 Loading

在正常情況下,我們的介面通常會很快完成。然而,考慮到不同網路狀況下,介面響應速度可能會變慢,從而增加使用者的等待時間。為了最佳化使用者體驗,我們可以在全域性請求中新增 Loading 提示,這將大大提升使用者體驗。

const showLoading = () => {
  uni.showLoading({
     title: '載入中',
  });
};

const hideLoading = () => {
  uni.hideLoading();
};

uni.addInterceptor("request", {
  invoke: function (request) {
    showLoading();
    return request;
  },
   complete() {
      hideLoading();
   }
});

這樣每個介面請求時都會觸發顯示 Loading。考慮到某些介面可能不需要顯示 Loading,我們可以允許使用者在定義介面時明確控制是否展示 Loading

const showLoading = (loading) => {
   uni.showLoading({
      title: '載入中',
   });
};

const hideLoading = (loading) => {
   uni.hideLoading();
};

uni.addInterceptor("request", {
  invoke: function (config) {
    if (config.loading) {
      showLoading();
    }
    return request;
  },
   complete() {
      hideLoading();
   }
});


const wrapRequest = ({
  url = '',
  data = {},
  method = 'GET',
  header = {},
  loading = true // 預設是展示 loading
} = {}) => {
  return uni.request({
    ...defaultConfig,
    url,
    data,
    method,
    loading,
    header: {
      ...defaultOptions.header,
      ...header,
    },
  });
};

為了解決介面請求很快時 Loading 閃爍的問題,我們可以新增一個延遲引數。如果請求時間超過 50ms(具體閥值可以自己去定義) 才顯示 Loading,否則就不展示:

const LOADING_DELAY = 50; // 50ms 延遲 
let loadingTimer;

const showLoading = () => {
   uni.showLoading({
     title: '載入中',
   });
};

const hideLoading = () => {
   uni.hideLoading();
};

uni.addInterceptor("request", {
  invoke: function (config) {
    if (config.loading) {
      loadingTimer = setTimeout(showLoading, LOADING_DELAY);
    }
    return config;
  },
   complete() {
      clearTimeout(loadingTimer);
      hideLoading();
   }
});

7. 完整程式碼如下

const defaultOptions = {
  timeout: 15000,
  dataType: 'json',
  header: {
    'content-type': 'application/json',
  },
};
const TOKEN_KEY = 'token';
const BASE_URL = '';
const LOGIN_INVALID_CODE_LIST = ['INVALID_TOKEN', 'EXPIRED_TOKEN'];
const SUCCESS = 'SUCCESS';
const LOADING_DELAY = 50; // 50ms 延遲
let loadingTimer;

const handleURL = (config) => {
  const { url } = config;
  if (!/https|http/.test(url)) {
    config.url = url.startsWith('/')
      ? `${BASE_URL}${url}`
      : `${BASE_URL}/${url}`;
  }
};

const handleToken = (config) => {
  const token = uni.getStorageSync(TOKEN_KEY);
  if (token) {
    config.header.Authorization = token;
  }
};

const showLoading = () => {
  uni.showLoading({
    title: '載入中',
  });
};

const hideLoading = () => {
  uni.hideLoading();
};

uni.addInterceptor('request', {
  invoke: function (config) {
    if (config.loading) {
      loadingTimer = setTimeout(showLoading, LOADING_DELAY);
    }
    handleURL(config);
    handleToken(config);
  },
  success(res) {
    const { data: resData } = res;
    const { code, message } = resData;
    if (code !== SUCCESS) {
      // 如果響應程式碼在登入無效程式碼列表中
      if (LOGIN_INVALID_CODE_LIST.includes(code)) {
        uni.showToast({
          title: message,
          icon: 'none',
        });
        uni.navigateTo({
          url: '/pages/login/login',
        });
        return;
      } else {
        // 處理其他錯誤程式碼
        return Promise.reject(resData);
      }
    }
    return Promise.resolve(resData);
  },
  complete() {
    clearTimeout(loadingTimer);
    hideLoading();
  },
});

const wrapRequest = ({
  url = '',
  data = {},
  method = 'GET',
  header = {},
  loading = true,
} = {}) => {
  return uni.request({
    ...defaultOptions,
    url,
    data,
    method,
    loading,
    header: {
      ...defaultOptions.header,
      ...header,
    },
  });
};

export const get = (params) => wrapRequest({ ...params, method: 'GET' });
export const post = (params) => wrapRequest({ ...params, method: 'POST' });
export const put = (params) => wrapRequest({ ...params, method: 'PUT' });
export const del = (params) => wrapRequest({ ...params, method: 'DELETE' });

8. 測試

import { get } from '@/utils/request';

get({
   url: "https://api.aigcway.com/aigc/chat-category/list"
}).then((res)=> {
    console.log(res);
})

輸出如下:

{
    "code": "SUCCESS",
    "message": "操作成功",
    "data": []
}

總結

我們完成了一個通用請求庫的封裝,這基本上可以滿足大多數業務需求。在具體請求中,狀態碼處理可以根據自身業務需求進行調整。

為了掌握上面的內容,需要掌握 uni.addInterceptoruni.request 執行的完整流程。以下是整理的不同情況下的流程圖,可以參考學習。

上面流程圖對應示例程式碼:

uni.addInterceptor('request', {
  invoke: function (config) {
  	console.log("interceptor invoke");
  },
  success(res) {
  	console.log("interceptor success");
  },
  complete() {
  	console.log("interceptor complete");
  },
});
uni.request({
	url: ""
}).then(()=> {
	console.log("then");
}).catch(()=> {
	console.log("catch");
}).finally(()=> {
	console.log("finally");
})

上面流程圖對應示例程式碼:

uni.addInterceptor('request', {
  invoke: function (config) {
  	console.log("interceptor invoke");
  },
  success(res) {
  	console.log("interceptor success");
  },
  complete() {
  	console.log("interceptor complete");
  },
});
uni.request({
	success(){
		console.log("success");
	},
	fail(){
		console.log("fail");
	},
	complete(){
		console.log("complete");
	}
});

如果大家覺得有幫助,請點贊、收藏、分享,謝謝!

相關文章