XMLHttpRequest—必知必會


前言

  • 做web開發,我們都知道瀏覽器通過XMLHttpRequest對象進行http通信
  • 在實際開發中我們使用的是各種框架封裝了的XMLHttpRequest對象,對具體實現往往一知半解.所以為了換框架好上手,請求有異常好調試,有必要深入學習一下XMLHttpRequest
  • 本文從基礎XMLHttpRequest開始,一步步把它封裝為更實用的框架級別

實例

  • 一個最簡單的http請求
let xhr = new XMLHttpRequest(); xhr.open('GET', '/url', true); xhr.send(); 
  • 一個稍微完整的http請求
 let xhr = new XMLHttpRequest(); // 請求成功回調函數 xhr.onload = e => { console.log('request success'); }; // 請求結束 xhr.onloadend = e => { console.log('request loadend'); }; // 請求出錯 xhr.onerror = e => { console.log('request error'); }; // 請求超時 xhr.ontimeout = e => { console.log('request timeout'); }; // 請求回調函數.XMLHttpRequest標准又分為Level 1和Level 2,這是Level 1和的回調處理方式 // xhr.onreadystatechange = () => { // if (xhr.readyState !== 4) { // return; // } // const status = xhr.status; // if ((status >= 200 && status < 300) || status === 304) { // console.log('request success'); // } else { // console.log('request error'); // } // }; xhr.timeout = 0; // 設置超時時間,0表示永不超時 // 初始化請求 xhr.open('GET/POST/DELETE/...', '/url', true || false); // 設置期望的返回數據類型 'json' 'text' 'document' ... xhr.responseType = ''; // 設置請求頭 xhr.setRequestHeader('', ''); // 發送請求 xhr.send(null || new FormData || 'a=1&b=2' || 'json字符串'); 
  • 很多東西一看就懂,但當真正去用的時候就會發現很多問題
  • 為了深入學習,本着使XMLHttpRequest更易用的原則,模仿jQuery ajax封裝XMLHttpRequest

封裝XMLHttpRequest

const http = { /** * js封裝ajax請求 * >>使用new XMLHttpRequest 創建請求對象,所以不考慮低端IE瀏覽器(IE6及以下不支持XMLHttpRequest) * >>使用es6語法,如果需要在正式環境使用,則可以用babel轉換為es5語法 https://babeljs.cn/docs/setup/#installation * @param settings 請求參數模仿jQuery ajax * 調用該方法,data參數需要和請求頭Content-Type對應 * Content-Type data 描述 * application/x-www-form-urlencoded 'name=哈哈&age=12'或{name:'哈哈',age:12} 查詢字符串,用&分割 * application/json name=哈哈&age=12' json字符串 * multipart/form-data new FormData() FormData對象,當為FormData類型,不要手動設置Content-Type * 注意:請求參數如果包含日期類型.是否能請求成功需要后台接口配合 */ ajax: (settings = {}) => { // 初始化請求參數 let _s = Object.assign({ url: '', // string type: 'GET', // string 'GET' 'POST' 'DELETE' dataType: 'json', // string 期望的返回數據類型:'json' 'text' 'document' ... async: true, // boolean true:異步請求 false:同步請求 required data: null, // any 請求參數,data需要和請求頭Content-Type對應 headers: {}, // object 請求頭 timeout: 1000, // string 超時時間:0表示不設置超時 beforeSend: (xhr) => { }, success: (result, status, xhr) => { }, error: (xhr, status, error) => { }, complete: (xhr, status) => { } }, settings); // 參數驗證 if (!_s.url || !_s.type || !_s.dataType || _s.async === undefined) { alert('參數有誤'); return; } // 創建XMLHttpRequest請求對象 let xhr = new XMLHttpRequest(); // 請求開始回調函數 xhr.addEventListener('loadstart', e => { _s.beforeSend(xhr); }); // 請求成功回調函數 xhr.addEventListener('load', e => { const status = xhr.status; if ((status >= 200 && status < 300) || status === 304) { let result; if (xhr.responseType === 'text') { result = xhr.responseText; } else if (xhr.responseType === 'document') { result = xhr.responseXML; } else { result = xhr.response; } // 注意:狀態碼200表示請求發送/接受成功,不表示業務處理成功 _s.success(result, status, xhr); } else { _s.error(xhr, status, e); } }); // 請求結束 xhr.addEventListener('loadend', e => { _s.complete(xhr, xhr.status); }); // 請求出錯 xhr.addEventListener('error', e => { _s.error(xhr, xhr.status, e); }); // 請求超時 xhr.addEventListener('timeout', e => { _s.error(xhr, 408, e); }); let useUrlParam = false; let sType = _s.type.toUpperCase(); // 如果是"簡單"請求,則把data參數組裝在url上 if (sType === 'GET' || sType === 'DELETE') { useUrlParam = true; _s.url += http.getUrlParam(_s.url, _s.data); } // 初始化請求 xhr.open(_s.type, _s.url, _s.async); // 設置期望的返回數據類型 xhr.responseType = _s.dataType; // 設置請求頭 for (const key of Object.keys(_s.headers)) { xhr.setRequestHeader(key, _s.headers[key]); } // 設置超時時間 if (_s.async && _s.timeout) { xhr.timeout = _s.timeout; } // 發送請求.如果是簡單請求,請求參數應為null.否則,請求參數類型需要和請求頭Content-Type對應 xhr.send(useUrlParam ? null : http.getQueryData(_s.data)); }, // 把參數data轉為url查詢參數 getUrlParam: (url, data) => { if (!data) { return ''; } let paramsStr = data instanceof Object ? http.getQueryString(data) : data; return (url.indexOf('?') !== -1) ? paramsStr : '?' + paramsStr; }, // 獲取ajax請求參數 getQueryData: (data) => { if (!data) { return null; } if (typeof data === 'string') { return data; } if (data instanceof FormData) { return data; } return http.getQueryString(data); }, // 把對象轉為查詢字符串 getQueryString: (data) => { let paramsArr = []; if (data instanceof Object) { Object.keys(data).forEach(key => { let val = data[key]; // todo 參數Date類型需要根據后台api酌情處理 if (val instanceof Date) { // val = dateFormat(val, 'yyyy-MM-dd hh:mm:ss'); } paramsArr.push(encodeURIComponent(key) + '=' + encodeURIComponent(val)); }); } return paramsArr.join('&'); } } 
  • 可以把這段代碼復制到你的ide上查看
  • 如果你對我封裝的看不懂,下面推薦幾篇文章你繼續看看
  • 調用http.ajax:發送一個get請求
http.ajax({ url: url + '?name=哈哈&age=12', success: function (result, status, xhr) { console.log('request success...'); }, error: (xhr, status, error) => { console.log('request error...'); } }); 
  • 調用http.ajax:發送一個post請求
http.ajax({ url: url, type: 'POST', data: {name: '哈哈', age: 12}, //或 data: 'name=哈哈&age=12', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, beforeSend: (xhr) => { console.log('request show loading...'); }, success: function (result, status, xhr) { console.log('request success...'); }, error: (xhr, status, error) => { console.log('request error...'); }, complete: (xhr, status) => { console.log('request hide loading...'); } }); 
  • 此時的http.ajax方法已經完全可以處理請求了,但是每個請求都要單獨處理異常情況嗎?如果需要請求前顯示loading請求結束關閉loading,每個請求都要添加beforeSendcomplete參數嗎?答案顯而易見,於是繼續封裝

封裝http.ajax

  • 給http對象添加了request方法,該方法添加了業務邏輯后然后調用http.ajax,詳情閱讀代碼及注釋
const http = { /** * 根據實際業務情況裝飾 ajax 方法 * 如:統一異常處理,添加http請求頭,請求展示loading等 * @param settings */ request: (settings = {}) => { // 統一異常處理函數 let errorHandle = (xhr, status) => { console.log('request error...'); if (status === 401) { console.log('request 沒有權限...'); } if (status === 408) { console.log('request timeout'); } }; // 使用before攔截參數的 beforeSend 回調函數 settings.beforeSend = (settings.beforeSend || function () { }).before(xhr => { console.log('request show loading...'); }); // 保存參數success回調函數 let successFn = settings.success; // 覆蓋參數success回調函數 settings.success = (result, status, xhr) => { // todo 根據后台api判斷是否請求成功 if (result && result instanceof Object && result.code !== 1) { errorHandle(xhr, status); } else { console.log('request success'); successFn && successFn(result, status, xhr); } }; // 攔截參數的 error settings.error = (settings.error || function () { }).before((result, status, xhr) => { errorHandle(xhr, status); }); // 攔截參數的 complete settings.complete = (settings.complete || function () { }).after((xhr, status) => { console.log('request hide loading...'); }); // 請求添加權限頭,然后調用http.ajax方法 (http.ajax.before(http.addAuthorizationHeader))(settings); }, // 添加權限請求頭 addAuthorizationHeader: (settings) => { settings.headers = settings.headers || {}; const headerKey = 'Authorization'; // todo 權限頭名稱 // 判斷是否已經存在權限header let hasAuthorization = Object.keys(settings.headers).some(key => { return key === headerKey; }); if (!hasAuthorization) { settings.headers[headerKey] = 'test'; // todo 從緩存中獲取headerKey的值 } } }; Function.prototype.before = function (beforeFn) { // eslint-disable-line let _self = this; return function () { beforeFn.apply(this, arguments); _self.apply(this, arguments); }; }; Function.prototype.after = function (afterFn) { // eslint-disable-line let _self = this; return function () { _self.apply(this, arguments); afterFn.apply(this, arguments); }; }; 
  • 調用http.request:發送一個get請求
http.request({ url: url + '?name=哈哈&age=12', success: function (result, status, xhr) { console.log('進行業務操作'); } }); 

如下圖可以看到調用http.request方法自動添加了請求權限頭,輸出了業務日志

 
 

 

  • 如果請求發生異常,如下圖可以看到輸出了異常日志
http.request({ url: url, timeout: 1000, success: function (result, status, xhr) { console.log('進行業務操作'); } }); 
 
 
  • 此時的http.request已經可以統一處理業務邏輯了.發送一個post方法如下,可以看到還是需要設置headers,經常使用jQuery的都知道,jQuert還有更簡化的get,post等方法,所以我們繼續封裝
http.request({ url: url, type: 'POST', data: {name: '哈哈', age: 12}, // data: 'name=哈哈&age=12', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, success: function (result, status, xhr) { console.log('進行業務操作'); } }); 

封裝http.request

  • 給http對象添加了get,post等方法,這些方法主要設置了默認參數然后調用http.request,詳情閱讀代碼及注釋
const http = { get: (url, data, successCallback, dataType = 'json') => { http.request({ url: url, type: 'GET', dataType: dataType, data: data, success: successCallback }); }, delete: (url, data, successCallback, dataType = 'json') => { http.request({ url: url, type: 'DELETE', dataType: dataType, data: data, success: successCallback }); }, // 調用此方法,參數data應為查詢字符串或普通對象 post: (url, data, successCallback, dataType = 'json') => { http.request({ url: url, type: 'POST', dataType: dataType, data: data, headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, success: successCallback }); }, // 調用此方法,參數data應為json字符串 postBody: (url, data, successCallback, dataType = 'json') => { http.request({ url: url, type: 'POST', dataType: dataType, data: data, headers: { 'Content-Type': 'application/json; charset=UTF-8' }, success: successCallback }); } }; 
  • 調用http.get發送get請求
http.get(url + '?name=哈哈&age=12', null, (result, status, xhr) => { console.log('進行業務操作'); }); 
 
 
  • 調用http.post發送post請求
http.post(url, {name: '哈哈', age: 12}, (result, status, xhr) => { console.log('進行業務操作'); }) 
 
 
  • 調用http.postBody發送post請求,參數是json字符串.后台接口以對象方式接受參數
http.postBody(url, JSON.stringify({ name: '哈哈', age: 12, birthday: dateFormat(new Date(), 'yyyy-MM-dd hh:mm:ss') }), (result, status, xhr) => { console.log('進行業務操作'); }); 
 
 
  • 至此,發送一個http請求已經很簡單了.

實例—傳FormData類型參數

  • 參數不要設置contentType請求頭,瀏覽器會自動設置contentType為'multipart/form-data'
let formData = new FormData(); formData.append('name', '哈哈'); formData.append('age', '123'); http.request({ url: url + id, type: 'POST', data: formData, success: function (result, status, xhr) { console.log('進行業務操作'); } }); 
 
 

實例—上傳文件

  • 把文件對象放到FormData參數中
     
     

     
     
  • 如果需要監控上傳進度,需要ajax方法,添加onprogress事件
 xhr.upload.addEventListener('progress', e => { console.log('上傳進度'); }); 

實例—文件分塊傳輸

 
 
  • 可以看到一張圖片分了三個請求發送,至於后台到底能不能接受到這張圖片,當然需要后台處理
     
     

最后

  • 完整http.js代碼已上傳github

  • 使用XMLHttpRequest Level 1標准的onreadystatechange方法注冊回調看這個ajax.js

其他

關於Fetch API

  • XMLHttpRequest不好用,所以各個框架都要將其封裝.規范制定者也知道不好用,所以就出了個Fetch API來代替XMLHttpRequest
  • 由於Fetch API目前的瀏覽器兼容性不行,所以現在還不被考慮使用,但是它真的很好用
  • 發送一個get請求代碼如下(是不是似曾相識的感覺,fetch方法返回Promise)
fetch(url + '?name=哈哈&age=12').then(res=>res.json()).then(data=>{ console.log(data); }); 

關於http2.0

  • http2.0主要是相對我們正在使用的http1.1性能方面的提升,語法方面繼續使用1.1的內容,只是更改了系統之間傳輸數據的方式,這些細節實現由瀏覽器和服務器實現.所以叫http1.2更合適.
  • 你的網站想用http2?首先你的網站要全面支持https,然后在服務器端(tomcat或nginx等)配置啟用http2
  • 點這里了解更多http2
  • 如何判斷某網站是否使用了http2?在某網站控制台執行如下代碼
(function(){ // 保證這個方法只在支持loadTimes的chrome瀏覽器下執行 if(window.chrome && typeof chrome.loadTimes === 'function') { var loadTimes = window.chrome.loadTimes(); var spdy = loadTimes.wasFetchedViaSpdy; var info = loadTimes.npnNegotiatedProtocol || loadTimes.connectionInfo; // 就以 「h2」作為判斷標識 if(spdy && /^h2/i.test(info)) { return console.info('本站點使用了HTTP/2'); } } console.warn('本站點沒有使用HTTP/2'); })(); 
  • 京東,天貓,Google都用了http2,百度,淘寶沒有用
     
     

     
     

關於http,XMLHttpRequest,Ajax的關系

  • http是瀏覽器和web服務器交換數據的協議,規范
    XMLHttpRequest javascript的一個對象,是瀏覽器實現的一組api函數(方法),使用這些函數,瀏覽器再通過http協議請求和發送數據
    Ajax不是一種技術,而是綜合多種技術實現交互的模式名稱:用html展示頁面+使用XMLHttpRequest請求數據+使用js操作dom


作者:yifan666
鏈接:https://www.jianshu.com/p/918c63045bc3
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM