实际上,在之前的文件中,已经逐渐对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
做些简单的GET
或POST
,基于方便,可以封装个get
、post
函数:
// 对 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
});
}
如此在需要简单的GET
或POST
时,只要提供相关实参就可以了,基本流程被封装起来了,get
或post
的好处是,在请求类型是application/x-www-form-urlencoded
,会自动创建查询字符串。至于GET
或POST
以外的请求,可以试着使用ajax
函数,使用它时必须自行处理 URL 与请求体,get
与post
实际上也是委托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.js、Evt-1.0.0.js与Style-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>
按我观看执行结果。