动态

详情 返回 返回

封裝 uniapp 請求庫的最佳實踐 - 动态 详情

背景

在前端開發中,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 = (request) => {
  const token = uni.getStorageSync(TOKEN_KEY)
  if (token) {
    request.header.Authorization = token;
  }
}
uni.addInterceptor("request", {
  invoke: function (config) {
    handleToken(config);
  }
});

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

const BASE_URL = ""; 

const handleURL = (request) => {
  const { url } = request;
  if (!/https|http/.test(url)) {
    request.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 = (request) => {
  const { url } = request;
  if (!/https|http/.test(url)) {
    request.url = url.startsWith('/')
      ? `${BASE_URL}${url}`
      : `${BASE_URL}/${url}`;
  }
};

const handleToken = (request) => {
  const token = uni.getStorageSync(TOKEN_KEY);
  if (token) {
    request.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": []
}

總結

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

為了掌握上面的內容,需要清晰瞭解整個請求的完整流程。以下是整理的不同情況下的流程圖,可以參考學習。

promise.jpg

callback.jpg

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

user avatar dingtongya 头像 grewer 头像 smalike 头像 nihaojob 头像 front_yue 头像 qingzhan 头像 kobe_fans_zxc 头像 aqiongbei 头像 littlelyon 头像 zourongle 头像 longlong688 头像 linx 头像
点赞 245 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.