實際上,在之前的文件中,已經逐漸對 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>