如何實現 axios 的自定義介面卡 adapter

蚊子部落格發表於2020-10-26

Axios 是一個非常優秀的基於 promise 的 HTTP 庫,可以用在瀏覽器和 node.js 中。並且提供了很多便捷的功能,例如:

  • 支援 Promise API
  • 攔截請求和響應
  • 轉換請求資料和響應資料
  • 取消請求
  • 自動轉換 JSON 資料
  • 客戶端支援防禦 XSRF

但如果我們想基於 axios 擴充套件一些自己的資料請求方式(例如 mock 資料,某些 APP 內專屬的資料請求方式等),並能夠使用上 axios 提供的便捷功能,該怎麼自定義一個介面卡 adapter;

1. 介面卡要實現的功能

我們在基於 axios 實現額外的資料模組時,應當與 axios 的模式進行對齊。因此在返回的資料格式上,實現的功能上儘量保持一致。

1.1 promise 和工具

所有的適配均應當實現為 Promise 方式。

而且,有些功能的實現,axios 將其下放到了介面卡中自己進行實現,例如

  1. url 的拼接:即 baseURL 和 url 的拼接,若存在 baseURL 且 url 為相對路徑,則進行拼接,否則直接使用 url;
  2. 引數的拼接:若是 get 請求,需要自行將 object 型別拼接為 url 引數的格式並與 url 拼接完成;

這是自己需要實現的兩個基本的工具方法。

1.2 響應的格式

這裡我們要注意到請求介面正常和異常的格式。

介面正常時:

const result = {
    status: 200, // 介面的http 狀態
    statusText: 'ok',
    config: 'config', // 傳入的config配置,原樣返回即可,方便在響應攔截器和響應結果中使用
    data: {}, // 真實的介面返回結果
};

介面異常時,我們可以看下 axios 原始碼中對錯誤資訊的處理createErrorenhanceError(createError 中呼叫了 enhanceError),首先會建立一個 error 例項,然後給這個 error 例項新增一個屬性:

module.exports = function enhanceError(error, config, code, request, response) {
    error.config = config;
    if (code) {
        error.code = code;
    }

    error.request = request;
    error.response = response;
    error.isAxiosError = true;

    error.toJSON = function toJSON() {
        return {
            // Standard
            message: this.message,
            name: this.name,
            // Microsoft
            description: this.description,
            number: this.number,
            // Mozilla
            fileName: this.fileName,
            lineNumber: this.lineNumber,
            columnNumber: this.columnNumber,
            stack: this.stack,
            // Axios
            config: this.config,
            code: this.code,
        };
    };
    return error;
};

可以看到,除了正常的錯誤資訊外,還加入了很多別的屬性,例如 request, response, config 等。這裡我們在自己實現介面卡時,最好也要這樣統一編寫,方便更上層的業務層統一處理,避免為單獨的介面卡進行特殊處理。

關於 1.1 和 1.2 中的內容,若不進行打包編譯,則需要自己實現。若還要通過 webpack 等打包工具編譯一下的,可以直接引用 axios 中的方法,不用自己實現了,參考官方基於 axios 實現的mock-axios。例如:

import axios from 'axios';
import buildURL from 'axios/lib/helpers/buildURL';
import isURLSameOrigin from 'axios/lib/helpers/isURLSameOrigin';
import btoa from 'axios/lib/helpers/btoa';
import cookies from 'axios/lib/helpers/cookies';
import settle from 'axios/lib/core/settle';
import createError from 'axios/lib/core/createError';

然後直接使用就行了,不用再進行二次開發。

1.3 超時設定

我們不能無限地等待第三方服務的響應,如果第三方服務無響應或者響應時間過長,應當適時的終止掉。在 axios 中,前端使用了XMLHttpRequest,在 node 端使用了http,來實現介面的請求,兩者都有超時的設定,可以設定 timeout 欄位來設定超時的時間,自動取消當前的請求。

像有的發起的請求,自己並沒有超時的設定,例如 jsonp,是用建立一個 script 標籤來發起的請求,這個請求必須等到伺服器有響應才會終止(成功或者失敗)。這時,就需要我們自己用一個setTimeout來模擬了,但這樣,即使返回給業務層說“超時了,已取消當前請求”,但實際上請求還在,只不過若超過規定時間,只是不再執行對應的成功操作而已。

1.4 主動取消請求

我們也會有很多並沒有到超時時間,就需要主動取消當前請求的場景,例如在請求返回之前就切換了路由;上次請求還沒響應前,又需要發出新的請求等。都需要主動地取消當前請求。

axios 中已經提供了取消請求的功能,我們只需要按照規則接入即可。我們來看下 XMLHttpRequest 請求器中是怎麼取消請求的,在寫自定義請求器時也可以照理使用。

lib/adapters/xhr.js#L158中:

// 若config中已經配置了cancelToken
if (config.cancelToken) {
    // Handle cancellation
    // 若在外城執行了取消請求的方法,則這裡將當前的請求取消掉
    config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
            return;
        }

        // xhr中使用abort方法取消當前請求
        request.abort();
        reject(cancel);
        // Clean up request
        request = null;
    });
}

我們在寫自己的介面卡時,也可以將這段拷貝過去,將內部取消的操作更換為自己的即可。

關於 cancel 操作執行的原理,這裡暫不展開,有興趣的可以參考這篇文章:axios cancelToken 原理解析

到這裡,若把上面的功能都實現了,就已經完成了一個標準的介面卡了。

2. 編寫自定義介面卡

每個人需要的介面卡肯定也不一樣,複雜度也不一樣,例如有的想接入小程式的請求,我自己想接入客戶端裡提供的資料請求方式等。我們這裡只是通過實現一個簡單的jsonp介面卡來講解下實現方式。

我們以 es6 的模組方式來進行開發。所有的實現均在程式碼中進行了講解。

// 這裡的config是axios裡所有的配置
const jsonpAdapter = (config) => {
    return new Promise((resolve, reject) => {
        // 是否已取消當前操作
        // 因jsonp沒有主動取消請求的方式
        // 這裡使用 isAbort 來標識
        let isAbort = false;

        // 定時器識別符號
        let timer = null;

        // 執行方法的名字,
        const callbackName = `jsonp${Date.now()}_${Math.random()
            .toString()
            .slice(2)}`;

        // 這裡假設已經實現了baseURL和url的拼接方法
        const fullPath = buildFullPath(config.baseURL, config.url);

        // 這裡假設已經實現了url和引數的拼接方法
        // 不太一樣的地方在於,jsonp需要額外插入一個自己的回撥方法
        const url = buildURL(
            fullPath,
            {
                ...config.params,
                ...{ [config.jsonpCallback || 'callback']: callbackName },
            },
            config.paramsSerializer
        );

        // 建立一個script標籤
        let script = document.createElement('script');

        // 成功執行操作後
        function remove() {
            if (script) {
                script.onload = script.onerror = null;

                // 移除script標籤
                if (script.parentNode) {
                    script.parentNode.removeChild(script);
                }
                // 取消定時器
                if (timer) {
                    clearTimeout(timer);
                }

                script = null;
            }
        }

        // 成功請求後
        window[callbackName] = (data) => {
            // 若已需要請求,則不再執行
            if (isAbort) {
                return;
            }

            // 返回的格式
            const response = {
                status: 200,
                statusText: 'ok',
                config,
                request: script,
                data: data,
            };
            remove();
            // 實際上這裡上一個settle操作,會額外判斷是否是合理的status狀態
            // 若我們在config.validateStatus中設定404是合理的,也會進入到resolve狀態
            // 但我們這裡就不實現這個了
            // settle(resolve, reject, response);
            resolve(response);
        };

        // 請求失敗
        script.onerror = function (error) {
            remove();

            reject(createError('Network Error', config, 404));
        };

        // 若設定了超時時間
        if (config.timeout) {
            timer = setTimeout(function () {
                remove();
                // 取消當前操作
                isAbort = true;
                reject(
                    createError(
                        'timeout of ' + config.timeout + 'ms exceeded',
                        config,
                        405
                    )
                );
            }, config.timeout);
        }

        // 若定義了取消操作
        if (config.cancelToken) {
            config.cancelToken.promise.then(function () {
                if (!script) {
                    return;
                }
                remove();
                isAbort = true;

                reject(createError('Cancel Error', config, 404));
            });
        }

        script.src = url;
        const target =
            document.getElementsByTagName('script')[0] || document.head;
        target.parentNode && target.parentNode.insertBefore(script, target);
    });
};

export default jsonpAdapter;

3. 將介面卡新增到 axios 中

axios 的 config 提供了 adapter 欄位讓我們插入自己的介面卡。使用自定義介面卡又有兩種情況:

  1. 完全只使用自定義的介面卡;
  2. 在某種情況下使用自定義介面卡,其他情況時還是使用 axios 自己的介面卡。

第 1 種情況還好,只需要 return 自己介面卡返回的結果結果即可;而第 2 種情況中,則有個小坑需要踩一下,我們這裡也只講解下第 2 種情況。我要把剛才實現的 jsonp 介面卡新增到 axios 中,並且只在引數有format=jsonp時才呼叫該介面卡,其他還是用的 axios 提供的介面卡。

import Axios from 'axios';
import jsonpAdapter from './jsonpAdater';

const request = Axios.create({
    adapter: (config) => {
        if (config?.params?.format === 'jsonp') {
            return jsonpAdapter(config);
        }

        // 這裡需要將config.adapter設定為空
        // 否則會造成無限迴圈
        return defaultAxios({ ...config, ...{ adapter: undefined } });
    },
});

使用方式,點選檢視 demo【axios 自定義的 jsonp 介面卡】:

使用自定義的介面卡 jsonp 發起請求。

// 使用自定義的介面卡jsonp發起請求
var options = {
    params: {
        format: 'jsonp',
    },
};
request(
    'https://api.prize.qq.com/v1/newsapp/answer/share/oneQ?qID=506336',
    options
)
    .then(function (response) {
        console.log('jsonp response', response);
    })
    .catch(function (error) {
        console.error('jsonp error', error);
    });

使用 axios 預設的介面卡發起請求。

// 使用axios預設的介面卡發起請求
request('https://api.prize.qq.com/v1/newsapp/answer/share/oneQ?qID=506336')
    .then(function (response) {
        console.log('axios response', response);
    })
    .catch(function (error) {
        console.error('axios error', error);
    });

4. 總結

這裡,我們就已經實現了一個自定義介面卡了,在滿足一定條件時可以觸發這個介面卡。通過這個思路,我們也可以實現一個自定義的 mock 方法,例如當引數中包含format=mock時則呼叫 mock 介面,否則就正常請求。

也歡迎關注我的公眾號,一起學習討論。

蚊子部落格的公眾號

相關文章