封装 Ajax 操作


实际上,在之前的文件中,已经逐渐对XMLHttpRequest的相关操作做了些封装了,现在需要的是,创建一个 Ajax-1.0.0.js,将先前已经做的一些封装放进去,并做一些补强,首先是对XMLHttpRequest的基本封装:

// 组合与编码请求参数
function params(paraObj) {
    return Object.keys(paraObj)
                 .map(name => {
                     let paraName = encodeURIComponent(name);
                     let paraValue = encodeURIComponent(paraObj[name]);                         
                     return `${paraName}=${paraValue}`.replace(/%20/g, '+');
                 })
                 .join('&');
}

class XHREventTarget {
    constructor(xhr) {
        let evtTypes = ['loadstart', 'progress', 'abort', 'error', 'load', 'time', 'loadend'];

        let handlers = evtTypes.reduce((handlers, evtType) => {
            handlers[evtType] = new Set();
            return handlers;
        }, {});

        evtTypes.forEach(evtType => {
            xhr[`on${evtType}`] = function(evt) {
                handlers[evtType].forEach(handler => handler.call(xhr, evt));
            };
        });

        this.xhr = xhr;
        this.handlers = handlers;
    }

    addEvt(evtType, handler) {
        this.handlers[evtType].add(handler);
        return this;
    }

    removeEvt(evtType, handler) {
        this.handlers[evtType].delete(handler);
        return this;
    }       
}

class XHRUpload extends XHREventTarget {
    constructor(xhr) {
        super(xhr);
    }
}

// 对 XMLHttpRequest 做简单封装
class XHR extends XHREventTarget {
    constructor() {
        super(new XMLHttpRequest());

        let xhr = this.xhr;
        let handlers = this.handlers;
        handlers['readystatechange'] = new Set();

        xhr.onreadystatechange = function(evt) {
            handlers['readystatechange']
                .forEach(handler => handler.call(xhr, evt));
        };
    }

    open(method, url, paraObj, async = true, username = null, password = null) {
        let openUrl = paraObj ? `${url}?${params(paraObj)}` : url; 
        this.xhr.open(method, openUrl, async, username, password);
        return this;
    }

    addHeaders(headers) {
        Object.keys(headers)
              .forEach(name => this.xhr.setRequestHeader(name, headers[name]));
        return this;
    }

    send(body = null) {
        this.xhr.send(body);
        return this;
    }

    uploadXHR() {
        if(this.upload === undefined) {
            this.upload = new XHRUpload(this.xhr.upload);
        }
        return this.upload;
    }

    set responseType(type) {
        this.xhr.responseType = type;
        return this;
    }

    get response() {
        return this.xhr.response;
    }
}

在我的想法中,XMLHttpRequest的操作有一定的复杂性,完全隐藏相关操作流程是不可能的,因此,这个XHR只是做简单封装,如果需要细部的操作,就使用XHR,你还是得知道相关操作流程,然而,利用XHR实例,可以在操作时使用流畅风格,而在事件上,也与先前文件中的事件处理风格一致。

当然,由于经常使用XMLHttpRequest做些简单的GETPOST,基于方便,可以封装个getpost函数:

// 对 Ajax 请求相关设定的封装
function ajax({method, url, headers = {}, body = null, responseType = '', handlers = {}}) {
    let request = new XMLHttpRequest();

    request.responseType = responseType;

    Object.keys(handlers).forEach(handler => {
        request[handler] = handlers[handler];
    });

    request.open(method, `${url}`);

    Object.keys(headers).forEach(header => {
        request.setRequestHeader(header, headers[header]);
    });

    request.send(body);    
}

// 方便的 get 函数,用于 GET 请求
function get(url, {headers = {}, paraObj = {}, responseType = '', handlers = {}}) {
    let targetUrl = Object.keys(paraObj).length === 0 ? url : `${url}?${params(paraObj)}`;

    ajax({
        method : 'GET',
        url  : targetUrl,
        headers,
        responseType,
        handlers
    });
}

// 方便的 post 函数,用于 POST 请求
function post(url, {headers = {}, body = null, responseType = '', handlers = {}}) {
    let bodyContent = body;
    if(headers['Content-Type'] === 'application/x-www-form-urlencoded' && typeof body !== 'string') {
        bodyContent = params(body);
    }

    ajax({
        method : 'POST',
        url,
        headers,
        body : bodyContent,
        responseType,
        handlers
    });
}

如此在需要简单的GETPOST时,只要提供相关实参就可以了,基本流程被封装起来了,getpost的好处是,在请求类型是application/x-www-form-urlencoded,会自动创建查询字符串。至于GETPOST以外的请求,可以试着使用ajax函数,使用它时必须自行处理 URL 与请求体,getpost实际上也是委托ajax来处理请求。

另一种封装的思路是,把XMLHttpRequest封装为像是 Fetch API,这会需要使用到Promise,会需要这么做的情况是,目标浏览器不支持,而你又打算使用 Fetch API,有兴趣知道怎么实现的话,可以参考fetch的 polyfill。

接下来把必要的名称导出:

export {params, XHR, ajax, get, post};

export default function(method, url, options) {
    if(method === undefined) {
        return new XHR();
    }

    switch(method.toLowerCase()) {
        case 'get':
            get(url, options);
            break;
        case 'post':
            post(url, options);
            break;
        case 'put':
        case 'delete':
        case 'head':
        case 'option':
        case 'trace':
            ajax({
                method,
                url,
                ...options
            });
            break;
        default:
            throw new Error('no such http method');
    }
};

在默认导出的部份,这边设计为可创建XHR实例,或者是可执行指定请求的工厂函数,判别的方式是简单地看看有无指定method

你可以在Ajax-1.0.0.js,以及XD-1.2.0.jsEvt-1.0.0.jsStyle-1.0.0.js下载到这系列文件中已创建之模块。

来使用这个程序库,实际改写一下〈使用 GET 请求〉中第一个范例(你的浏览器必须支持 ES6 模块):

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
</head>
<body>
    图书:<br>
    <select id="category">
        <option>-- 选择分类 --</option>
        <option value="theory">理论基础</option>
        <option value="language">程序语言</option>
        <option value="web">网页技术</option>
    </select><br><br>
    采购:<div id="book"></div>

<script type="module"> 
    import {elemsById} from './js/XD-1.2.0.js';   
    import {get} from './js/Ajax-1.0.0.js';

    elemsById('category').addEvt('change', evt => {
        let paraObj = {
            category : evt.target.value,
            time     : new Date().getTime()
        };
        let handlers = {
            onload(evt) {
                let req = evt.target;
                if(req.status === 200) {
                    elemsById('book').html(req.responseText);
                }
            }
        };

        get('GET-1.php', {
            paraObj, 
            handlers
        });        
    });

</script>

</body>
</html>

按我观看执行结果

接下来是改写〈使用 POST 请求〉中的范例:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
</head>
<body>

    新增书签:<br>
    网址:<input id="url" type="text">
    <span id="message" style="color:red"></span><br>
    名称:<input type="text">

<script type="module">    
    import {elemsById} from './js/XD-1.2.0.js';
    import {post} from './js/Ajax-1.0.0.js';

    elemsById('url').addEvt('blur', evt => {
        let headers = {
            'Content-Type' : 'application/x-www-form-urlencoded'
        };
        let body = {
            url : document.getElementById('url').value
        };
        let handlers = {
            onload(evt) {
                let req = evt.target;
                if(req.status === 200 && req.responseText === 'existed') {
                    elemsById('message').html('URL 已存在');
                }  
            }
        };

        post('POST-1.php', {
            headers,
            body,
            handlers
        });    
    });
</script>

</body>
</html>

按我观看执行结果

这个post函数也可以处理文件上传,例如改写〈结合 FormData 上传文件〉中第一个范例:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
</head>
<body>

    <form id="f" action="upload" method="post" enctype="multipart/form-data">
          Photo  :<input type="file" name="photo"/><br>
        <input id="upload" type="submit"/>
    </form> 

    <span id="message"></span>

<script type="module">
    import {elemsById} from './js/XD-1.2.0.js';
    import http from './js/Ajax-1.0.0.js';

    elemsById('upload').addEvt('click', evt => {
        let formData = new FormData(document.getElementById('f'));

        http('POST', 'upload', {
            body     : formData,
            handlers : {
                onload(evt) {
                    let req = evt.target;
                    if(req.status === 200) {
                        elemsById('message').html('File Uploaded');
                    }                          
                }
            }
        });

        evt.preventDefault();
    });

</script>

  </body>
</html>

上头使用的是默认导出的工厂函数,由于指定'POST',因此底层会使用post函数,接着来改写〈使用 responseType〉中搜寻框的范例:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <style type="text/css">
        div {
            color: #ffffff;
            background-color: #ff0000;
            border-width: 1px;
            border-color: black;
            border-style: solid;
            position: absolute;
        }    
    </style>
</head>
<body>
    <hr>
    搜寻:<input id="search" type="text">

<script type="module">
    import x from './js/XD-1.2.0.js';
    import http from './js/Ajax-1.0.0.js';

    let doc = x(document);
    let search = doc.elemsById('search');

    search.addEvt('keyup', evt => {
        doc.elemsByTag('div').remove();

        let value = search.val();

         // 没有输入值,直接结束
        if(value === '') {
            return;
        }

        http('GET', `ResponseType-1.php?keyword=${value}`, {
            responseType : 'json',
            handlers     : {
                onload(evt) {

                    let request = evt.target;

                    if(request.status === 200) {
                        // response 会是 JSON 对象
                        let keywords = request.response;

                        // 字符串数组长度不为0时加以处理
                        if(keywords.length !== 0) {
                            let innerHTML = keywords.map(keyword => `${keyword}<br>`)
                                                    .join('');

                            let offset = search.offset();
                            let offsetWidth = search.attr('offsetWidth');
                            let offsetHeight = search.attr('offsetHeight');

                            // 创建容纳选项的<div>
                            let div = x('div').toElemCollection()
                                              .html(innerHTML)
                                              .css({
                                                  left  : `${offset.x}px`,
                                                  top   : `${offset.y + offsetHeight}px`,
                                                  width : `${offsetWidth}px`
                                              });

                            document.body.appendChild(div.get());
                        }
                    }            

                }            
            }
        });
    });

</script>

</body>
</html>

按我观看执行结果


展开阅读全文