亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

Ajax 封裝

前言

學會了 Ajax 的請求以及如何處理服務端的響應。這一章節,我們著重來封裝一個簡單的 Ajax。

前置知識:

  1. 本章節會使用部分 ES6 語法
  2. 本章節使用 Promise

簡單需求:

  • 支持 Promise 語法處理結果
  • 支持自定義配置,包括 headers
  • 內置 url、params、 data、headers 處理

1. 構造一個這樣的 xhr

function xhr(config) {
    return new Promise((resolve, reject) => {
        const request = new XMLHttpRequest();

        /**
     	* 調用 open 方法
     	*/
        request.open(method, url);

        request.onreadystatechange = function handleLoad() {
            if (request.readyState !== 4) return
            if (request.status === 0) return
            const responseData = request.response
            resolve(responseData)
        }

        request.send(data)
    });
}

首先, 我們的 xhr 函數支持 config 傳入, 內部通過 XMLHttpRequest 技術來進行請求的收發, 大致就是上面這樣結構的代碼,內部的實現我們前面章節都講過,唯一不同的是,在 onreadystatechange 上,我們掛載的方法最后使用 resolve() 來進行斷言,這樣做的目的是,后續可以通過 .then() 的方式進行數據操作。

1.1 method 標準化

首先, 用戶傳進來的 method 可能是大寫也可能是小寫,我們可以先做一個標準化,對 method 做一個轉化,將其變為大寫:

method.toUpperCase()

1.2 構建 url

有些同學很奇怪,為什么說構建 url,我們不是通過 config 傳入 url 嗎?

是的,但是同學你別忘了,我們支持 params!

因此,我們需要把 params 上的參數進行一定格式序列化拼接到 url 后面 ,構成 "url?a=xxx&b=xxx" 的格式。為此,我們需要提供了一個 buildUrl 的函數:

/**
 * 構建 url
 * @param {*} url
 * @param {*} params
 */
function buildUrl(url, params) {
    if (!params || !isPlainObject(params)) return url; // 如果 params 沒有傳或者不是一個純對象,直接返回原 url
    let values = [];
    Object.keys(params).forEach(key => {
        // 對 params 中的每一項進行處理
        const val = params[key];
        if (typeof val === undefined || val === null) {
            // 如果當前項的值為 undefined 或者 null,則忽略
            return;
        }
        values.push(`${key}=${val}`); // 將 “key=value”的形式加入到 values 數組中
    });
    let serializedParams = values.join("&"); // 序列化,將 values 數組轉化為字符串,格式為 "key=value&key=value"
    if (serializedParams) {
        // 如果有值,則加入到url后面。構成 "url?key=value&key=value" 的形式
        url += (url.indexOf("?") === -1 ? "?" : "&") + serializedParams;
    }
    return url;
}

在這個函數中,我們可以傳參 url 和 params。如果傳入params 為假值,那我們直接忽略,返回 url 即可。否則,我們需要對 params 中 的每一項目進行序列化,變為 "key=vaue" 這樣的形式, 添加到 values 數組中。接著我們通過數組的 .join("&") 的方法,把 values 數組通過 “&” 進行拼接。最后拼接到 url 后面,構成 "url?key=value&key=value" 的形式返回。

這里,我們也涉及到了一個工具函數 isPlainObject,在本章節中好幾處都會用到,他的作用是判斷該對象是不是一個純 “{}” 的對象,它的實現如下:

const toString = Object.prototype.toString; // 由于 Object.prototype.toString 在判斷類型的時候非常好用,并且用到的次數經常會比較多,我們通??梢赃@樣緩存起來

/**
 * 判斷當前 val 是否是一個純對象
 * @param {*} val
 */
function isPlainObject(val) {
    return toString.call(val) === "[object Object]";
}

1.3 標準化 data

因為 .send() 是無法支持 Json 格式數據的,所以我們需要對 data 做一個序列化處理:

/**
 * 處理 data,因為 send 無法直接接受 json 格式數據,這里我們可以直接序列化之后再傳給服務端
 * @param {*} data 
 */
function transformData (data) {
    if (isPlainObject(data)) {
        return JSON.stringify(data)
    }
    return data
}

實現非常簡單,如果判斷 data 是一個純對象的話,就加一道 JSON.stringify(data) 的操作進行序列化, 否則直接返回 data 本身。

1.4 設置 headers

對于 headers 的操作,我們會著重對 Content-Type 進行處理,在沒有 Content-Type 的時候,我們應該有個默認的支持。因為 headers 屬性上是大小寫不敏感的,因此我們會對 Content-Type 做一個統一處理:

function transformHeaders (headers) {
    const contentTypeKey = 'Content-Type' // Content-Type 的 key 值常量
    if (isPlainObject(headers)) {
        Object.keys(headers).forEach(key => {
            if (key !== contentTypeKey && key.toUpperCase() === contentTypeKey.toLowerCase()) {
                // 如果 key 的大寫和 contentTypeKey 的大寫一致,證明是同一個,這時就可以用 contentTypeKey 來替代 key 了
                headers[contentTypeKey] = headers[key]
                delete headers[key]
            }
        })
        if (!headers[contentTypeKey]) {
            // 如果最后發現沒有 Content-Type,那我們就設置一個默認的
            headers[contentTypeKey] = 'application/json;charset=utf-8'
        }
    }
}

// 在 function xhr 中
// 設置頭部
transformHeaders(headers)
Object.keys(headers).forEach(key => {
    if (!data && key === 'Content-Type') {
        delete headers[key]
        return
    }
    request.setRequestHeader(key, headers[key])
})

transformHeaders 函數對 headers 進行了一定程度的轉化,包括為 Content-Type 提供了默認的支持,這里默認為 "application/json;charset=utf-8"。在 xhr 函數中,我們還會對headers的每一項進行判斷,如果沒有 data ,那我們會刪除 Content-Type。同時,我們會調用 setRequestHeader 方法將 headers 屬性添加到頭部。

1.5 設置響應類型

if (responseType) {
    // 如果設置了響應類型,則為 request 設置 responseType
    request.responseType = responseType;
}

1.6 設置超時時間

if (timeout) {
    // 如果設置超時時間, 則為 request 設置 timeout
    request.timeout = timeout;
}

1.7 處理結果

// 狀態變化處理函數
request.onreadystatechange = function handleLoad() {
    if (request.readyState !== 4) return;
    if (request.status === 0) return;
    
    // 獲取響應數據
    const responseData =
          request.responseType === "text"
    ? request.responseText
    : request.response;
    if (request.status >= 200 && request.status < 300 || request.status === 304) {
        // 成功則 resolve 響應數組
        resolve(responseData);
    } else {
        // 失敗則 reject 錯誤原因
        reject(new Error(`Request failed with status code ${request.status}`));
    }
};

// 錯誤處理事件
request.onerror = function hadleError() {
    //reject 錯誤原因
    reject(new Error('Network Error'))
}

// 超時處理事件
request.ontimeout = function handleTimeout() {
    // reject 錯誤原因
    reject(new Error(`Timeout of ${timeout} ms exceeded`))
}

處理結果分為幾個部分:

  1. 正常處理服務端響應
  2. 請求錯誤
  3. 請求超時

其中,正常處理服務端響應還要判斷狀態碼,這里判斷正確的是 200 至 300 之間狀態碼,再一個是 304 緩存。此時我們會通過 resolve 斷言數據。否則,通過 reject 來斷言失敗原因。

1.8 xhr 函數

至此,我們會得到這樣一個 xhr 函數:

function xhr(config) {
    return new Promise((resolve, reject) => {
        const {
            url,
            method = "get",
            params = {},
            data = null,
            responseType,
            headers,
            timeout
        } = config;
        const request = new XMLHttpRequest();

        /**
     * 調用 open 方法
     * method.toUpperCase() 的作用主要是講 method 都標準統一為大寫字母狀態。 比如 'get'.toUpperCase() 會返回 'GET'
     */
        request.open(method.toUpperCase(), buildUrl(url, params));

        if (responseType) {
            // 如果設置了響應類型,則為 request 設置 responseType
            request.responseType = responseType;
        }

        if (timeout) {
            // 如果設置超時時間, 則為 request 設置 timeout
            request.timeout = timeout;
        }

        // 設置頭部
        transformHeaders(headers);
        Object.keys(headers).forEach(key => {
            if (!data && key === "Content-Type") {
                delete headers[key];
                return;
            }
            request.setRequestHeader(key, headers[key]);
        });

        request.onreadystatechange = function handleLoad() {
            if (request.readyState !== 4) return;
            if (request.status === 0) return;
            const responseData =
                  request.responseType === "text"
            ? request.responseText
            : request.response;
            if (request.status >= 200 && request.status < 300 || request.status === 304) {
                resolve(responseData);
            } else {
                reject(new Error(`Request failed with status code ${request.status}`));
            }
        };

        request.onerror = function hadleError() {
            reject(new Error("Network Error"));
        };

        request.ontimeout = function handleTimeout() {
            reject(new Error(`Timeout of ${timeout} ms exceeded`));
        };

        request.send(transformData(data));
    });
}

2. 創建 Ajax

有了 xhr ,我們當然希望 Ajax 能夠提供一些默認配置。這里的 Ajax 函數不做太過復雜的功能,但我們會簡單模擬支持默認 config。

事實上,最后在 Ajax 中,內部調用的就是 xhr 函數。類似這個樣子:

function Ajax(config) {
	// code ...

    return xhr(config);
}

2.1 提供默認 config

首先,我們來定義默認配置

// 默認配置
const defaultconf = {
    method: "get",
    timeout: 500,
    headers: {
        Accept: "application/json, text/plain, */*"
    }
};

// 為 headers 上添加一些方法的默認 headers, 暫時掛在 headers[method] 下
["get", "delete", "options", "head"].forEach(method => {
    defaultconf.headers[method] = {};
});

// 為 headers 上添加一些方法的默認 headers, 暫時掛在 headers[method]["put", "post", "patch"].forEach(method => {
    defaultconf.headers[method] = {
        "Content-Type": "application/x-www-form-urlencoded"
    };
});

這里我們提供了默認的配置,包括默認的 method、 timeout、 headers 等,其中,get、 delete、 options、 head 的 headers 默認為空;而 put、 post 和 patch 涉及到 data 傳送的會給一個默認的配置: "Content-Type": "application/x-www-form-urlencoded"

2.2 合并配置

const method = config.method || defaultconf.method; // 請求的方法名

// 合并 headers
const headers = Object.assign(
    {},
    defaultconf.headers,
    defaultconf[method],
    config.headers || {}
);

// 合并默認配置和自定義配置,這里簡單的進行后者對前者的覆蓋
const conf = Object.assign({}, defaultconf, config);

conf.headers = headers; // 配置的 headers 為我們上面合并好的 headers

// 刪除 conf 配置中,headers 下默認的方法的headers塊
["get", "delete", "options", "head", "put", "post", "patch"].forEach(key => {
    delete conf.headers[key];
});

如上所示,我們會通過方法名獲取方法名對應的默認的 headers,并與傳入配置 headers 和默認 headers 進行合并。然后我們會合并配置。最后我們不要忘了把合并后的配置中,headers 中方法名對應的配置塊刪除。

2.3 Ajax 函數

最后,我們會得到這樣一個 Ajax:

function Ajax(config) {

    const method = config.method || defaultconf.method;

    const headers = Object.assign(
        {},
        defaultconf.headers,
        defaultconf[method],
        config.headers || {}
    );

    const conf = Object.assign({}, defaultconf, config);

    conf.headers = headers;
    ["get", "delete", "options", "head", "put", "post", "patch"].forEach(key => {
        delete conf.headers[key];
    });

    return xhr(conf);
}

3.簡單的示例

3.1 請求的代碼塊

// 服務端現有接口,進行 post 請求
Ajax({
    method: 'post',
    url: '/simple/post',
    data: {
        a:1,
        b:2
    }
}).then(data => {
    console.log(data)
}).catch(e => {
    console.log('/simple/post', e)
})


// 服務端暫時沒有的接口, 進行 post 請求
Ajax({
    method: 'post',
    url: '/test/post',
    data: {
        a:1,
        b:2
    }
}).then(data => {
    console.log(data)
}).catch(e => {
    console.log('/test/post', e)
})

// 服務端現有接口, 進行 get 請求
Ajax({
    url: '/simple/get',
    params: {
        c:1,
        d:2
    }
}).then(data => {
    console.log(data)
}).catch(e => {
    console.log('/simple/get', e)
})

3.2 請求結果

圖片描述

圖片描述

如圖所示,請求正確接口的 Ajax 請求都得到了正確的返回。而訪問服務端暫時沒有的接口則返回了 404 錯誤。同時,GET 請求中沒有顯式提供 method,默認配置也能夠及時生效,默認為 GET。

4.小結

本章節到此為止,關于 Ajax 的封裝,核心技術使用的依然是 XMLHttpRequest 技術。在自定義 Ajax 中,我們可以提供多種屬性和方法來豐富和強壯我們的方法,比方說,我們可以提供 默認配置、Promise 語法支持、錯誤檢測及處理、參數標準化 等等。

本章節的 Ajax 依然是不完美的,有興趣的同學可以思考一下還能怎樣去封裝。至少我們還可以提供 request 和 response 的攔截和處理,我們也可以優化 config 合并策略。希望這能夠發動同學們的腦洞風暴!