zoukankan      html  css  js  c++  java
  • Ajax五

    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 合并策略。希望这能够发动同学们的脑洞风暴!

  • 相关阅读:
    通知
    KVO详解
    KVC详解
    KVC/KVO总结
    结构体Struct
    检测文件(夹)大小
    NSFileHandle&&NSFileManage
    ***NSFileManager
    获取文件扩展名
    MySql数据库_03
  • 原文地址:https://www.cnblogs.com/liunaiming/p/13052313.html
Copyright © 2011-2022 走看看