根據JavaScript中原生的XMLHttpRequest實現jQuery的Ajax

雲崖君發表於2021-08-09

基本介紹

XmlHttpRequest

XmlHttpRequest是JavaScript中原生的,歷史悠久的一種傳送網路請求的方案。

基本上所有前端框架對於網路請求的部分都是基於它來完成的。

在本章節中我們將瞭解它的基本用法,並且會實現一個與jQuery.ajax功能百分之九十相似的網路請求元件。

我是沒有讀過jQuery.ajax原始碼的,並且我也不是一個專精前端的程式設計師,所以後面的程式碼寫的可能不太好,僅和大家在此做個交流。

後端程式碼

為了方便後續程式碼測試,我們後端採用Python-flask框架來完成。

  • IP:localhost
  • PORT:5700

程式碼如下:

from logging import debug, error
from flask import Flask
from flask import request
from flask import make_response
from flask import jsonify

app = Flask(__name__)


@app.after_request  # 解決CORS跨域請求
def cors(response):
    response.headers['Access-Control-Allow-Origin'] = "*"
    if request.method == "OPTIONS":
        # 允許的請求頭
        response.headers["Access-Control-Allow-Headers"] = "Origin,Content-Type,Cookie,Accept,Token,authorization,user_head"
    return response


@app.route("/get", methods=["get"])
def get():
    user_head = request.headers.get("user_head")
    user_params = request.args
    print(user_params)
    return jsonify(user_params, user_head)


@app.route("/post", methods=["post"])
def post():
    user_head = request.headers.get("user_head")
    user_params = request.form
    print(user_params)
    return jsonify(user_params, user_head)


@app.route("/json", methods=["post"])
def json():
    user_head = request.headers.get("user_head")
    user_params = request.json
    return jsonify(user_params, user_head)


@app.route("/file", methods=["post"])
def file():
    file_obj = request.files.get("avatar")
    if file_obj is None:
        return make_response(jsonify("unload avatar"), 400)
    file_name = file_obj.filename
    file_obj.save(file_name)
    return jsonify("upload avatar success!!")


if __name__ == "__main__":
    app.run(host="localhost", port=5700, debug=True)

open()

open()方法用於建立並返回一個請求物件(單純建立,並不傳送)。

引數釋義:

  • method:請求方式
  • url:請求地址
  • async:是否非同步請求

注意:如果method為GET方式,則需要我們手動在url中新增引數

請求相關

send()

send()方法用於傳送一次網路請求。

引數釋義:

  • body:method為POST時攜帶的請求體資料,必須是字串型別

注意:如果method為GET方式,則直接xhr.send(null)即可,因為GET請求沒有請求體

setRequestHeader()

setRequestHeader()方法用於設定請求頭。

引數釋義:

  • header:請求頭的key
  • value:請求頭的value

注意:每次只能設定一組請求頭鍵值對,如果要設定多組請求頭鍵值對請多次呼叫該方法

abort()

abort()方法用於取消本次網路請求。

響應相關

getAllResponseHeaders()

getAllResponseHeaders()方法用於獲取所有響應頭資料。

返回的資料型別為字串。

getResponseHeader()

getResponseHeader()方法用於獲取響應頭中指定header的value。

返回的資料型別為字串。

status

status屬性用於返回服務端響應的HTTP響應狀態碼。

以下是常見的HTTP響應狀態碼:

  • 200:請求成功
  • 404:請求失敗,請求的資源錯誤
  • 500:請求失敗,伺服器內部出現錯誤
  • 302:重定向
  • 304:快取優先

statesText

statesText屬性用於返回服務端響應的狀態文字。

如OK、NotFound等字樣。

responseText

responseText屬性用於返回響應體主體內容。

返回的資料型別為字串。

回撥函式

readyState

readyState屬性用於返回一個整數型別的狀態值,它表明當前xhr物件的請求狀態。

共有5種形態:

  • 0:未初始化,尚未呼叫open()方法
  • 1:已初始化,呼叫了open()方法,但還未呼叫send()方法
  • 2:已傳送,呼叫了send()方法,但還未收到服務端響應
  • 3:已接收,已經收到了部分服務端響應資料
  • 4:已完成,已經收到了全部服務端響應資料

readystatechange

readystatechange事件用於設定xhr物件的readState狀態發生改變時所執行的回撥函式。

簡單使用

contentType說明

contentType是HTTP協議中的一個請求頭,它包含了瀏覽器告知後端伺服器本次傳送資料的格式。

它和<form>表單的enctype引數類似,共有以下一些常見值的選項:

常見的媒體格式型別如下:

  • text/html : HTML格式
  • text/plain :純文字格式
  • text/xml : XML格式
  • image/gif :gif圖片格式
  • image/jpeg :jpg圖片格式
  • image/png:png圖片格式

以application開頭的媒體格式型別:

  • application/xhtml+xml :XHTML格式
  • application/xml: XML資料格式
  • application/atom+xml :Atom XML聚合格式
  • application/json: JSON資料格式
  • application/pdf:pdf格式
  • application/msword : Word文件格式
  • application/octet-stream : 二進位制流資料(如常見的檔案下載)
  • application/x-www-form-urlencoded : <form enctype=“”>中預設的enctype,form表單資料被編碼為key/value格式傳送到伺服器(表單預設的提交資料的格式)

另外一種常見的媒體格式是上傳檔案之時使用的:

  • multipart/form-data : 在表單中進行檔案上傳時,就需要使用該格式

我們接下來會使用2種最主流的格式進行試驗:

  • application/x-www-form-urlencoded (預設的GET和POST資料格式)
  • application/json(前端需要傳送的JSON格式資料)

GET請求

傳送GET請求:

"use strict";

let xhr = new XMLHttpRequest();

// 繫結回撥函式
xhr.addEventListener("readystatechange", () => {
    switch (xhr.readyState) {
        case 0:
            console.log("未初始化,尚未呼叫open()方法");
            break
        case 1:
            console.log("已初始化,呼叫了open()方法,但還未呼叫send()方法");
            break
        case 2:
            console.log("已傳送,呼叫了send()方法,但還未收到服務端響應");
            break
        case 3:
            console.log("已接收,已經收到了部分服務端響應資料");
            break
        default:
            console.log("已完成,已經收到了全部服務端響應資料");
            if (xhr.statusText === "OK") {
                console.log("請求成功");
                console.log(xhr.status);
                console.log(xhr.statusText);
                console.log(xhr.responseText);
            } else {
                console.log("請求失敗");
                console.log(xhr.status);
                console.log(xhr.statusText);
                console.log(xhr.responseText);
            }
    }
})

// 請求方式,請求地址與引數,是否開啟非同步提交  提交的資料格式為url編碼
xhr.open("GET", "http://localhost:5700/get?username=Jack&userage=18", true);

// 設定請求頭,提交的資料格式為url編碼
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset-UTF-8');
xhr.setRequestHeader('user_head', 'HELLO WORLD');

// 傳送請求
xhr.send(null);

POST請求

傳送POST請求:

"use strict";

let xhr = new XMLHttpRequest();

// 繫結回撥函式
xhr.addEventListener("readystatechange", () => {
    switch (xhr.readyState) {
        case 0:
            console.log("未初始化,尚未呼叫open()方法");
            break
        case 1:
            console.log("已初始化,呼叫了open()方法,但還未呼叫send()方法");
            break
        case 2:
            console.log("已傳送,呼叫了send()方法,但還未收到服務端響應");
            break
        case 3:
            console.log("已接收,已經收到了部分服務端響應資料");
            break
        default:
            console.log("已完成,已經收到了全部服務端響應資料");
            if (xhr.statusText === "OK") {
                console.log("請求成功");
                console.log(xhr.status);
                console.log(xhr.statusText);
                console.log(xhr.responseText);
            } else {
                console.log("請求失敗");
                console.log(xhr.status);
                console.log(xhr.statusText);
                console.log(xhr.responseText);
            }
    }
})

// 請求方式,請求地址與引數,是否開啟非同步提交
xhr.open("POST", "http://localhost:5700/post", true);

// 設定請求頭,提交的資料格式為url編碼
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset-UTF-8');
xhr.setRequestHeader('user_head', 'HELLO WORLD');

// 傳送請求,提交的資料格式為url編碼
xhr.send("username=Jack&userage=18");

JSON格式

JSON格式的資料只能由POST請求發起,並且contentType應設定為appliction/json。

示例如下:

"use strict";

let xhr = new XMLHttpRequest();

// 繫結回撥函式
xhr.addEventListener("readystatechange", () => {
    switch (xhr.readyState) {
        case 0:
            console.log("未初始化,尚未呼叫open()方法");
            break
        case 1:
            console.log("已初始化,呼叫了open()方法,但還未呼叫send()方法");
            break
        case 2:
            console.log("已傳送,呼叫了send()方法,但還未收到服務端響應");
            break
        case 3:
            console.log("已接收,已經收到了部分服務端響應資料");
            break
        default:
            console.log("已完成,已經收到了全部服務端響應資料");
            if (xhr.statusText === "OK") {
                console.log("請求成功");
                console.log(xhr.status);
                console.log(xhr.statusText);
                console.log(xhr.responseText);
            } else {
                console.log("請求失敗");
                console.log(xhr.status);
                console.log(xhr.statusText);
                console.log(xhr.responseText);
            }
    }
})

// 請求方式,請求地址與引數,是否開啟非同步提交
xhr.open("POST", "http://localhost:5700/json", true);

// 設定請求頭
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('user_head', 'HELLO WORLD');

// 傳送請求
xhr.send(JSON.stringify({ "k1": "v1", "k2": "v2" }));

瞭解jQuery.ajax的原理

實現程式碼

原生的XmlHttpRequest所提供的方法太過於底層,因此我們可以對其進行封裝。

這裡以jQuery的Ajax為藍本,實現一個類似功能的小元件,也是為了方便後期更加了解jQuery.Ajax()做準備。

我們準備實現的功能:

  • 實現傳送GET請求
  • 實現傳送POST請求
  • 實現更加方便的新增請求頭
  • 實現傳送JSON格式的請求資料
  • 實現自動反序列化JSON格式的響應資料
  • 實現檔案上傳

實現如下:

"use strict";

class Ajax {

    constructor({ url, contentType, headers, data, beforeSend, dataFilter, success, error, complete, dataType = "", processData = true, type, method = type, traditional = false }) {

        // url:傳送請求的地址,String型別
        // type:傳送請求的方式,String型別,它和method都可以使用
        // method:傳送請求的方式,String型別,它和type都可以使用
        // contentType:資料編碼格式,相當於form表單的enctype,String型別
        // headers:設定的請求頭,Object型別
        // dataType:接收的響應資料型別,如果是json則自動進行反序列化,String型別
        // data:傳送的資料,post請求方式傳送在請求體內,get請求傳送會將其新增在url後,Object型別
        // processData:如果為true,瀏覽器將採用application/x-www-form-urlencoded方式對資料進行編碼,如果為false則按照傳入的contentType為準,bool型別
        // traditional:如果前端傳遞的是一個Array,如{"k1":[1,2,3,4]}則需要新增一個屬性traditional:true,否則後端將接收不到該引數。(實際上接受的時候要使用request.POST.get("k1[]"))來進行接受,這是有問題的

        this.xhr = new XMLHttpRequest();
        this.url = url;
        this.type = (type || method).toUpperCase();
        this.method = method.toUpperCase();
        this.headers = headers;
        this.contentType = processData && !contentType ? "application/x-www-form-urlencoded" : contentType;
        this.data = data;
        this.dataType = dataType.toLowerCase();
        this.traditional = traditional;

        // beforeSend 在傳送請求之前呼叫,並且傳入一個XMLHttpRequest作為引數。
        // dataFilter 在請求成功之後呼叫。傳入返回的資料以及"dataType"引數的值。並且必須返回新的資料(可能是處理過的)傳遞給success回撥函式,也就是說它會在success與error之前呼叫
        // success 在請求成功之後呼叫。傳入返回後的資料,以及包含成功程式碼的字串
        // error 在請求出錯時呼叫。傳入XMLHttpRequest物件,描述錯誤型別的字串以及一個異常物件(如果有的話)
        // complete 當請求完成之後呼叫這個函式,無論成功或失敗。傳入XMLHttpRequest物件,以及一個包含成功或錯誤程式碼的字串

        this.beforeSend = beforeSend || function (xhr) { };

        // 該函式預設會自動的根據dataType物件responseText進行解碼
        this.dataFilter = dataFilter || function (responseText, dataType) {
            switch (dataType) {
                case "application/json": case "json":
                    // 新增,this.xhr.responseJSON屬性
                    this.xhr.responseJSON = JSON.parse(responseText);
                    return this.xhr.responseJSON;
                default:
                    return responseText;
            }
        };
        this.success = success || function (responseText, statusText) { };
        this.error = error || function (responseText, statusText) { };
        this.complete = complete || function (xhr, statusText) { };



        // 繫結回撥函式
        this.bindStateChangeEvent();
        // 設定傳送的資料併傳送
        this.setRequestData();
        // 設定傳送方式
        this.setXhrOpen();
        // 設定請求頭
        this.setRequestHeader();
        // 傳送請求
        this.sendRequest();
    }

    bindStateChangeEvent() {
        let resultText = "";
        let statusText = "";

        // 繫結回撥函式,這裡採用匿名函式使this指向Object,否則this將指向xhr
        this.xhr.addEventListener("readystatechange", () => {
            switch (this.xhr.readyState) {
                case 1:
                    this.beforeSend(this.xhr);
                    break
                case 4:
                    resultText = this.dataFilter(this.xhr.responseText, this.dataType || this.xhr.getResponseHeader("Content-Type").toLowerCase());
                    if (this.xhr.statusText === "OK") {
                        statusText = "success";
                        this.success(resultText, statusText);
                    } else {
                        statusText = "error";
                        this.error(this.xhr, "error");
                    }
                    this.complete(this.xhr, statusText);
                    break
            }
        });
    }

    setRequestData() {
        // 判斷是否要進行url編碼,有2種情況:
        // 1.contentType是application/x-www-form-urlencoded時需要url編碼
        // 2.傳入的物件不是serialize()方法已經序列化好後的物件時不需要url編碼
        if (this.contentType === "application/x-www-form-urlencoded" && !Object.isFrozen(this.data)) {
            this.data = this.urlEncode(this.data);
        }
    }

    urlEncode() {
        return Object.entries(this.data).reduce((prev, cur, index, array) => {
            // key
            let key = cur[0];
            // value:2種情況,如果value是普通字串,則直接進行encodeURIComponent編碼處理,如果是陣列將按照 k1=v1&k2=v2 的方式進行編碼,並且判斷是否傳入traditional
            let value = this.checkValueIsArray(cur);

            if (array.length - 1 === index) {
                // 判斷是否是最後一位,如果是最後一位就加上&
                // traditional:如果前端傳遞的是一個Array,如{"k1":[1,2,3,4]}則需要新增一個屬性traditional:true,否則後端將接收不到該引數。(實際上接受的時候要使用request.POST.get("k1[]"))來進行接受,這是有問題的
                return !this.traditional && cur[1] instanceof Array ? prev += `${key}[]=${value}` : prev += `${key}=${value}`;
            } else {
                return !this.traditional && cur[1] instanceof Array ? prev += `${key}[]=${value}&` : prev += `${key}=${value}&`;
            }
        }, "");

    }

    checkValueIsArray(cur) {
        if (cur[1] instanceof Array) {
            cur[1].forEach((value, index, array) => {
                if (index != array.length - 1) {
                    value = encodeURIComponent(value);
                    array[index] = value + "&";
                } else {
                    array[index] = String(value);
                }
            })

            // traditional:如果前端傳遞的是一個Array,如{"k1":[1,2,3,4]}則需要新增一個屬性traditional:true,否則後端將接收不到該引數。(實際上接受的時候要使用request.POST.get("k1[]"))來進行接受,這是有問題的,這裡主要對陣列中的第2個元素開始及其之後的所有元素進行url編碼格式轉換
            return this.traditional ? cur[1].join(`${cur[0]}=`) : cur[1].join(`${cur[0]}[]=`)
        }
        else {
            return encodeURIComponent(cur[1])
        }
    }

    setXhrOpen() {
        // 如果請求方式是GET請求,則在url後加上編碼後的data,否則將只傳遞this.url
        // true引數為開啟非同步呼叫,也是預設的選項
        this.xhr.open(
            this.method,
            this.method === "GET" ? this.url.concat("?", this.data) : this.url,
            true
        );
    }

    setRequestHeader() {
        // 在不上傳檔案的時候,請求頭Content-Type是必須的
        for (let [k, v] of Object.entries(this.headers)) {
            this.xhr.setRequestHeader(k, v);
        }
        if (this.contentType) {
            this.xhr.setRequestHeader("Content-Type", this.contentType);
        }
    }

    sendRequest() {
        // 傳送請求時,如果請求方式為GET將使用this.xhr.send(null),否則將使用this.xhr.send(this.data)
        this.xhr.send(this.method === "GET" ? null : this.data);
    }
}

function getAjax(ajaxSettings) {
    return new Ajax(ajaxSettings);
}


function serialize(selector) {
    // 整體思路:先獲取一個包含所有表單項物件,再將物件進行url編碼

    let formNode = document.querySelector(selector);
    let haveValueNodeObject = {};
    Array.prototype.forEach.call(formNode.elements, (element, index, lst) => {
        switch (true) {
            // 多選或者單選select都將組成列表
            case element.tagName === "SELECT":
                Array.prototype.forEach.call(element.options, (option, index, array) => {
                    if (option.selected) {
                        haveValueNodeObject[element.name] ? haveValueNodeObject[element.name].push(option.value) : haveValueNodeObject[element.name] = [option.value];
                    }
                })
                break

            // 多選,組成列表
            case element.type === "checkbox":
                if (element.checked) {
                    haveValueNodeObject[element.name] ? haveValueNodeObject[element.name].push(element.value) : haveValueNodeObject[element.name] = [element.value];
                }
                break

            // 單選
            case element.type === "radio":
                if (element.checked) {
                    haveValueNodeObject[element.name] = element.value
                }
                break

            // 其他的專案,注意檔案物件不獲取
            default:
                if (element.name && element.type !== "file") {
                    haveValueNodeObject[element.name] = element.value || "";
                }
        }
    }
    )

    // 凍結物件,代表已經經過序列化了,在傳送資料時已避免url二次編碼
    // 這裡是傳入一個物件並且繼承Ajax的原型物件,因為這樣做才能在編碼時使用checkValueIsArray方法
    return Object.freeze(Ajax.prototype.urlEncode.call(
        { data: haveValueNodeObject, traditional: true, __proto__: Ajax.prototype },
    ));
}

function serializeArray(selector) {
    // 提取form表單中的資料,並將其構建為一個name:value的陣列 [{name:value}, {name:value}, {name:value}]

    let formNode = document.querySelector(selector);
    let haveValueArray = [];
    Array.prototype.forEach.call(formNode.elements, (element, index, lst) => {
        switch (true) {

            case element.tagName === "SELECT":
                Array.prototype.forEach.call(element.options, (option, index, array) => {
                    if (option.selected) {
                        haveValueArray.push({ "name": element.name, "value": option.value });
                    }
                })
                break

            case element.type === "checkbox" || element.type === "radio":
                if (element.checked) {
                    haveValueArray.push({ "name": element.name, "value": element.value });
                }
                break

            default:
                if (element.name && element.type !== "file") {
                    haveValueArray.push({ "name": element.name, "value": element.value });
                }
        }
    }
    )
    return haveValueArray;
}

window.$ = { ajax: getAjax, serialize, serializeArray };

支援的上傳格式

由於我們JavaScript的原生Ajax高度按照jQuery.ajax作為藍本。所以常見的特性要與其保持一致。

如支援的原生上傳資料格式實現:

  • 僅支援上傳k-v的物件,不支援上傳陣列
  • 前端上傳的資料中,不允許出現物件巢狀的形式。如{"k1":{"k1-1":v1}},這樣只會得到{“k1” : “object”}
  • 如果前端傳遞的是一個Array,如 {"k1":[1, 2, 3, 4]} 則需要新增一個屬性 traditional:true,否則後端將接收不到該引數。(實際上接受的時候要使用request.POST.get("k1[]"))來進行接受,這是有問題的

如果你上傳的資料是json格式,當然就不會出現這些問題了。

如何傳送GET請求

傳送GET請求示例如下:

// 傳送GET請求
$.ajax({
    url: "http://localhost:5700/get",
    method: "get",
    headers: {
        "user_head": "Hello World",
    },
    data: { "name": "Jack", "age": 18 },
    dataType: "json",
    success: (responseText, statusText) => {
        console.log(responseText);
        console.log(statusText);
    },
    error: (responseText, statusText) => {
        console.log(responseText);
        console.log(statusText);
    },
    complete: (xhr, statusText) => {
        console.log("此處可獲取響應頭");
    }
})

如何傳送POST請求

傳送POST請求示例如下:

 // 傳送post請求
 $.ajax({
    url: "http://localhost:5700/post",
    method: "post",
    headers: {
        "user_head": "Hello World",
    },
    data: { "name": "Jack", "age": 18 },
    dataType: "json",
    success: (responseText, statusText) => {
        console.log(responseText);
        console.log(statusText);
    },
    error: (responseText, statusText) => {
        console.log(responseText);
        console.log(statusText);
    },
    complete: (xhr, statusText) => {
        console.log("此處可獲取響應頭");
    }
})

如何傳送JSON資料

傳送JSON格式資料示例如下:

// 傳送json格式資料
$.ajax({
    url: "http://localhost:5700/json",
    method: "post",
    headers: {
        "user_head": "Hello World",
    },
    data: JSON.stringify({ "name": "Jack", "age": 18 }),    // 1.手動進行JSON格式化
    dataType: "json",
    contentType: "application/json",                        // 2.請求頭中宣告本次傳送的是JSON格式資料
    success: (responseText, statusText) => {
        console.log(responseText);
        console.log(statusText);
    },
    error: (responseText, statusText) => {
        console.log(responseText);
        console.log(statusText);
    },
    complete: (xhr, statusText) => {
        console.log("此處可獲取響應頭");
    }
})

如何提交form表單的資料

針對form表單提交的資料,我們可以使用封裝好的2個函式serialize()和serializeArray()。

  • serialize():提取form表單中的資料項,並對其做url編碼處理,返回一個字串,注意,它不會提取檔案選擇框

  • serializeArray():提取form表單中的資料,並將其構建為一個name:value的陣列,注意,它不會提取檔案選擇框,最終格式為 [{name:value}, {name:value}, {name:value}],

示例如下,如果是serialize()則直接提交即可:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <form action="#" id="register">
        <div>
            <label for="username">使用者名稱</label>
            <input type="text" id="username" name="username" required>
        </div>

        <div>
            <label for="password">密碼</label>
            <input type="password" id="password" name="password" required>
        </div>
        <div>
            <label for="male">男</label>
            <input type="radio" name="gender" value="male" id="male" required>
            <label for="female">女</label>
            <input type="radio" name="gender" value="female" id="female">
        </div>

        <div>
            <label for="basketball">籃球</label>
            <input type="checkbox" name="hobby" value="basketball" id="basketball" required>
            <label for="football">足球</label>
            <input type="checkbox" name="hobby" value="football" id="football">
            <label for="volleyball">排球</label>
            <input type="checkbox" name="hobby" value="volleyball" id="volleyball">
        </div>

        <div>
            <label for="city">城市</label>
            <select name="city" id="city" multiple>
                <option value="beijing">北京</option>
                <option value="shanghai">上海</option>
                <option value="shenzhen">深圳</option>
            </select>
        </div>

        <div>
            <label for="avatar">上傳頭像</label>
            <input type="file" name="avatar" id="avatar">
        </div>

        <div>
            <button type="button">提交</button>
        </div>

    </form>
</body>
<script src="./demo.js"></script>
<script>
    document.querySelector("button").addEventListener("click", (event) => {
        $.ajax({
            url: "http://localhost:5700/post",
            method: "post",
            headers: {
                "user_head": "Hello World",
            },
            data: $.serialize("#register"),
            dataType: "json",
            success: (responseText, statusText) => {
                console.log(responseText);
                console.log(statusText);
            },
            error: (responseText, statusText) => {
                console.log(responseText);
                console.log(statusText);
            },
            complete: (xhr, statusText) => {
                console.log("此處可獲取響應頭");
            }
        })

        console.log($.serialize("#register"));
        // username=%E4%BA%91%E5%B4%96&password=123&gender=male&hobby=basketball&hobby=football&city=shanghai&city=shenzhen
    })

</script>

</html>

如果是serializeArray(),需要使用appliction/json的方式進行提交,因為該方法返回的是一個陣列:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <form action="#" id="register">
        <div>
            <label for="username">使用者名稱</label>
            <input type="text" id="username" name="username" required>
        </div>

        <div>
            <label for="password">密碼</label>
            <input type="password" id="password" name="password" required>
        </div>
        <div>
            <label for="male">男</label>
            <input type="radio" name="gender" value="male" id="male" required>
            <label for="female">女</label>
            <input type="radio" name="gender" value="female" id="female">
        </div>

        <div>
            <label for="basketball">籃球</label>
            <input type="checkbox" name="hobby" value="basketball" id="basketball" required>
            <label for="football">足球</label>
            <input type="checkbox" name="hobby" value="football" id="football">
            <label for="volleyball">排球</label>
            <input type="checkbox" name="hobby" value="volleyball" id="volleyball">
        </div>

        <div>
            <label for="city">城市</label>
            <select name="city" id="city" multiple>
                <option value="beijing">北京</option>
                <option value="shanghai">上海</option>
                <option value="shenzhen">深圳</option>
            </select>
        </div>

        <div>
            <label for="avatar">上傳頭像</label>
            <input type="file" name="avatar" id="avatar">
        </div>

        <div>
            <button type="button">提交</button>
        </div>

    </form>
</body>
<script src="./demo.js"></script>
<script>
    document.querySelector("button").addEventListener("click", (event) => {
        $.ajax({
            url: "http://localhost:5700/json",
            method: "post",
            headers: {
                "user_head": "Hello World",
            },
            data: JSON.stringify($.serializeArray("#register")),    // 1.手動進行JSON格式化
            dataType: "json",
            contentType: "application/json",                        // 2.請求頭中宣告本次傳送的是JSON格式資料
            success: (responseText, statusText) => {
                console.log(responseText);
                console.log(statusText);
            },
            error: (responseText, statusText) => {
                console.log(responseText);
                console.log(statusText);
            },
            complete: (xhr, statusText) => {
                console.log("此處可獲取響應頭");
            }
        })


        console.log($.serializeArray("#register"));
        // Array(7)
        // 0: {username: "雲崖"}
        // 1: {password: "123"}
        // 2: {gender: "male"}
        // 3: {hobby: "basketball"}
        // 4: {hobby: "football"}
        // 5: {city: "shanghai"}
        // 6: {city: "shenzhen"}
    })

</script>

</html>

如何進行檔案上傳

如果要傳送檔案,我們需要藉助FormData物件進行資料提交,以下是注意事項。

  • 在<form>表單中上傳檔案,必須要將enctype設定為multipart/form-data。

  • 但是在使用XmlHttpRequest物件上傳檔案時,並不需要指定 contentType為multipart/form-data 格式,所以不新增contentType請求頭。

  • contentType應設定為false,即不使用任何資料格式,不使用任何編碼

  • processData應設定為false,不讓瀏覽器做任何資料格式的編碼

示例如下,我們使用FormData搭配serializeArray()方法實現一個真正意義上的非同步提交表單:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <form action="#" id="register">
        <div>
            <label for="username">使用者名稱</label>
            <input type="text" id="username" name="username" required>
        </div>

        <div>
            <label for="password">密碼</label>
            <input type="password" id="password" name="password" required>
        </div>
        <div>
            <label for="male">男</label>
            <input type="radio" name="gender" value="male" id="male" required>
            <label for="female">女</label>
            <input type="radio" name="gender" value="female" id="female">
        </div>

        <div>
            <label for="basketball">籃球</label>
            <input type="checkbox" name="hobby" value="basketball" id="basketball" required>
            <label for="football">足球</label>
            <input type="checkbox" name="hobby" value="football" id="football">
            <label for="volleyball">排球</label>
            <input type="checkbox" name="hobby" value="volleyball" id="volleyball">
        </div>

        <div>
            <label for="city">城市</label>
            <select name="city" id="city" multiple>
                <option value="beijing">北京</option>
                <option value="shanghai">上海</option>
                <option value="shenzhen">深圳</option>
            </select>
        </div>

        <div>
            <label for="avatar">上傳頭像</label>
            <input type="file" name="avatar" id="avatar">
        </div>

        <div>
            <button type="button">提交</button>
        </div>

    </form>
</body>
<script src="./demo.js"></script>
<script>
    document.querySelector("button").addEventListener("click", (event) => {

        // 獲取上傳的檔案物件
        let fileNode = document.querySelector("#avatar");
        let fileObj = fileNode.files[0];

        // 使用FormData用於偽造form表單提交的資料
        let fd = new FormData();

        // 新增檔案
        fd.append(fileNode.name, fileObj);

        // 新增其他表單項
        $.serializeArray("#register").forEach((obj, index, array) => {
            fd.append(obj.name, obj.value);
        });

        // 傳送json格式資料
        $.ajax({
            url: "http://localhost:5700/file",
            method: "post",
            headers: {
                "user_head": "Hello World",
            },
            data: fd,            // 直接傳送ForData物件即可
            dataType: "json",
            contentType: false,  // 必須設定為false
            processData: false,  // 必須設定為false
            success: (responseText, statusText) => {
                console.log(responseText);
                console.log(statusText);
            },
            error: (xhr, statusText) => {
                console.log(xhr.responseText);
                console.log(statusText);
            },
            complete: (xhr, statusText) => {
                console.log(statusText);
            }
        })
    })
</script>

</html>

相關文章