[全端筆記]Javascript 在瀏覽器端和Node環境下的請求差異

比較fetch和axios的原理與差異

Posted by 李定宇 on Sunday, June 9, 2024

前言

在撰寫前/後端服務的時候,通常會很自然的使用axios或者是fetch來發起api請求,但其中的差異是如何?

Axios

無論在前端還是後端服務,都可以直接使用axios來發起請求,這是因為axios本身對瀏覽器和Node環境各自做了處理與封裝,然後透過單一接口暴露出去。在瀏覽器端,axios是使用XMLHttpRequest(XHR)來處理請求、而在Node環境下使用http/https library。以下是基於axios原始碼的簡化版demo

  • 瀏覽器端的處理—xhrAdapter函數,返回一個基於 XMLHttpRequest 的Promise

    export async function xhrAdapter(config) {
    return new Promise((resolve, reject) => {
        const request = new XMLHttpRequest();
        request.open(config.method.toUpperCase(), config.url, true);
    
        Object.keys(config.headers).forEach((key) => {
            request.setRequestHeader(key, config.headers[key]);
        });
    
        request.onload = function () {
            const response = {
                data: JSON.parse(request.responseText),
                status: request.status,
                statusText: request.getAllResponseHeaders(),
                config: config,
                request: request,
            };
            resolve(response);
        };
        request.onerror = function () {
            reject(new Error("Network Error"));
        };
    
        request.ontimeout = function () {
            reject(new Error(`Timeout of ${config.timeout} ms exceeded`));
        };
    
        request.send(config.data);
    });
    }
    
  • Node環境端處理—httpAdpater函數,返回一個基於 http或者是 https 的Promise

    import http from "http";
    import https from "https";
    
    export async function httpAdpater(config) {
    return new Promise((resolve, reject) => {
        const url = new URL(config.url);
        const isHttps = url.protocol === "https";
        const options = {
            method: config.method,
            headers: config.headers,
        };
    
        const request = (isHttps ? https : http).request(
            url,
            options,
            (response) => {
                let data = "";
    
                response.on("data", (chunk) => {
                    data += chunk;
                });
                response.on("end", () => {
                    resolve({
                        data: JSON.parse(data),
                        status: response.statusCode,
                        statusText: response.statusMessage,
                        headers: response.headers,
                        config: config,
                        request: response,
                    });
                });
            },
        );
    
        request.on("error", (error) => {
            reject(error);
        });
        if (config.data) {
            request.write(config.data);
        }
    
        request.end();
    });
    }
    
    
  • 最後,在對全局的環境進行判別—假如為Browser環境,便返回xhrAdapter;如果是Node環境,則返回httpAdpater

    async function getDefaultAdapter() {
    if (typeof XMLHttpRequest !== "undefined") {
        // for browser, using XHR adapter
        const { xhrAdapter } = await import("./xhrAdapter.js");
    
        return xhrAdapter;
    } else if (typeof process !== "undefined") {
        // for Node env, using HTTP Adpater
        const { httpAdpater } = await import("./httpAdpater.js");
    
        return httpAdpater;
    }
    
    throw new Error("No suitable adapter found");
    }
    
  • 完整mock axios程式碼在 link

fetch

2015年之後,Browser 環境下推出了新的Fetch API,成為Web 應用程式的異步呼叫標準;然而,在 Node.js v21 之前,並沒有相對應的Fetch API,因此許多SDK會使用node-fetch這個library來對Node環境添加Fetch API。

基本上,也是對全局環境的判別—如果是Browser環境,就直接用原生的fetch;如果是Node環境,就動態載入node-fetch 依賴(如果直接import的話,在browser環境會有編譯錯誤),然後將fetch掛載到全局

export async function initFetch() {
    let fetch;

    if (typeof window !== "undefined" && typeof window.fetch === "function") {
        fetch = window.fetch;
    } else {
        const nodeFetch = await import("node-fetch");
        fetch = nodeFetch.default;
    }

    if (typeof global !== "undefined") {
        global.fetch = fetch;
    } else if (typeof window !== "undefined") {
        window.fetch = fetch;
    }
}

然而,近期 node-fetch更新版本成只支持 ESM ,所以在使用 commonjs 編譯的NodeJS專案中並沒辦法使用。這時,可以使用 http/https 依賴來替換—

import http from "http";
import https from "https";
import { URL } from "url";

function fetch(url, options = {}) {
    return new Promise((resolve, reject) => {
        const urlObj = new URL(url);
        const isHttps = urlObj.protocol === "https:";
        const { method = "GET", headers = {}, body } = options;

        const requestOptions = {
            method,
            headers,
        };

        const request = (isHttps ? https : http).request(
            urlObj,
            requestOptions,
            (response) => {
                let data = "";

                response.on("data", (chunk) => {
                    data += chunk;
                });

                response.on("end", () => {
                    const responseData = {
                        ok:
                            response.statusCode >= 200 &&
                            response.statusCode < 300,
                        status: response.statusCode,
                        statusText: response.statusMessage,
                        headers: response.headers,
                        url: response.url || url,
                        json: () => Promise.resolve(JSON.parse(data)),
                        text: () => Promise.resolve(data),
                        blob: () => Promise.resolve(Buffer.from(data)),
                    };
                    resolve(responseData);
                });
            },
        );

        request.on("error", (error) => {
            reject(error);
        });

        if (body) {
            request.write(body);
        }

        request.end();
    });
}

export default fetch;

然後再將全局的fetch指向該 fetch 函數

  • 完整mock fetch程式碼在 link

小結

許多第三方依賴、SDK等在涉及網路請求的時候,不太會用到Axios,主要是考慮到減少其他的依賴減少打包體積;同時,由於Node.js 原生的Fetch API是 v21後才有的,如果考慮到向前兼容的話,還是需要手動封裝一下http/https、或者直接使用node-fetch

完整程式碼在github js-fetch_vs_axios

Ref

ChangeLog

  • 20240609-初稿