# Ajax 技術與用法實利 Ajax的全名是Asynchronous JavaScript and XML,也就是非同步的Javascript與XML技術。以往傳統的網頁大多都是同步的,也就是每當使用者進行了一個操作或請求,伺服器端就會傳回一個完整的新頁面,使用者在收到新頁面後才能進行下一步的操作。 然而使用者因此可能經常需要花費許多時間等待伺服器回傳結果,尤其當網頁的數據量大時,使用者的體驗將會非常糟糕。 為了解決這個問題,非同步處理的技術應運而生,網頁不再需要隨著每個操作重新刷新,而是可以做到局部更新,對於不需要刷新整個頁面的操作我們可以通過局部更新的方式來向伺服器發請求,大服的提升傳輸效率以及改善使用者體驗。而在JS網頁上最常被使用的非同步技術就是Ajax。 ## 1. XHR 目前AJAX的技術有兩個比較常見的分支,分別是較為廣泛使用的XHR跟Fetch,而XHR底下又有兩種常用封裝jQuery跟axios,首先我們就先來看看原始的XHR吧。 XHR是一個物件,全名為XMLHttpRequest,使用流程是先設定好URL與method後將request送出,等待伺服器回傳response,接著通過callback function處理後續動作,一個基本的XHR請求程式碼如下所示: ``` var xhr = new XMLHttpRequest() xhr.open('get','http://xxxx') xhr.send() xhr.onload = function(){ console.log(xhr.responseText) } ``` 除了open,send,onload以外,XHR還有幾個常用的函式或方法,下面依序介紹 * **onreadystatechange** XHR物件中有一個readyState的狀態碼,它依照順序總共有5種狀態,分別是 0:剛產生XHR物件 1:調用open(),但請求尚未送出 2:調用send()送出請求 3:等待資料回傳 4:資料回傳完畢 而onreadystatechange顧名思義就當readyState改變時調用該方法,我們可以通過它來針對不同狀態時的處理回應 ``` var xhr = new XMLHttpRequest() xhr.open('get','http://xxxx') xhr.onreadystatechange = function(){ //等待資料回傳的過程中顯示Loading... if(xhr.readyState == 3) console.log("loading...") else if(xhr.readyState == 4){ //接收到伺服器回應後跟據響應狀態碼,成功接收到回傳數據則顯示結果 if(xhr.status >= 200 && xhr.status < 300){ console.log(xhr.responseText) } } } xhr.send() ``` * **setRequestHeader()** 這個函數的作用是設定請求頭,裡面會包含兩個參數(header, value),我們以一個POST請求來做示範 ``` var xhr = new XMLHttpRequest(); xhr.open('post', 'http://xxxx); xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); xhr.send('name=Bob&password=123456'); ``` * **responseType** 用來設定回傳數據的格式,預設值是text,常用的有json,text,document,arraybuffer等等 * **onerror** 與onload相反,onload是在請求成功時調用的方法,而onerror則是在請求失敗時調用的方法 ``` var xhr = new XMLHttpRequest() xhr.open('get','http://xxxx') xhr.send() xhr.onload = function(){ console.log(xhr.responseText) } xhr.onerror = function(){ console.log('請求出錯') } ``` ## 2. jQuery 上一段我們可以看到直接使用XHR發送請求的方法非常粗糙,一個請求中包還了太多的任務,需要撰寫許多行程式碼,一旦一個網頁中需要包含多個請求,那我們就需要撰寫大量的程式碼,這將嚴重影響程式的可讀性以及後續維護的難度。 jQuery提供了$.load,$.get,$.post等函數替我們重新封裝了XHR,大幅簡化了發送請求的過程,下面將依序介紹jQuery中的Ajax函數。 ### .load(url,data,complete) load函式用來動態載入文件並插入DOM中,預設是以GET方式發送請求,但若在data參數給予傳遞參數,那就會改成POST,complete則是請求完成時的回調函數(成功與否都會執行) ``` $('#result').load('http://xxxx') $('#result').load('http://xxxx',{name:"Tom"}) $('#result').load('http://xxxx',function(data){ console.log(data) }) ``` ### $.get(url,data,success,dataType) get是一個簡單的非同步GET請求,success回調函數僅會在成功時執行,若要處理失敗時的執行(error),那就必須使用$.ajax() ``` $.get('http://xxxx', function(data){ $('#result').append(data) }) ``` ### $.post(url,data,success,dataType) 簡單的非同步POST請求,使用方法跟get一樣 ``` $.post('http://xxxx', {name:"Tom"}, function(data){ $('#result').append(data) }) ``` ### $.ajax(settings) jQuery最底層的Ajax物件,上面幾個簡單應用都是由$.ajax()包裝而成。ajax只有一個參數settings,settings是一個JSON物件,其中包含許多ajax的參數選項,下面是一個的使用範例 ``` $ajax({ type:'POST', url:'http://xxxx', data:{name:"Tom"}, success: function(){ console.log("成功") }, error: function(){ console.log("失敗") } }) ``` ## 3. Promise 在繼續往下講之前有必要先介紹下Promise這個異步函數,這對於後面要介紹的Fetch以及axios來說是非常重要的基礎。 Promise是一個異步處理的解決方案,它可以重新包裝異步函數,並解決回調地獄的問題。 首先先看看下面這個例子吧 ``` fs.readFile('./1.html', (err, data1) => { if(err) throw err fs.readFile('./2.html', (err, data2) => { if(err) throw err fs.readFile('./3.html', (err, data3) => { if(err) throw err console.log(data1 + data2 + data3) }) }) }) ``` 假設我們要處理一連串的程式,而且每個步驟都需要依賴上一個步驟的結果,那麼隨著步驟一多,就會形成像上面一樣的回調地獄,程式碼不停的向後縮進,判斷式一個包著一個,數量一多將會嚴重影響程式的判讀,而Promise就是為了解決這個問題而出現。 首先先介紹一個簡單的Promise範例 ``` let p = new Promise((resolve, reject)=>{ fs.readFile('./content.txt', (err,data)=>{ if(err) reject(err) resolve(data) }) }) p.then(value=>{ console.log(value.toString()); }, reason=>{ console.log(reason); }) ``` 這是一個讀取資料的簡單範例,將原本的讀取程式的程式碼包裹進Promise物件中,然後通過then函數來處理成功跟失敗的回調 接下來,我們來看看Promise的運作機制,下面這張圖是Promise物件的內容 ![](https://i.imgur.com/ccO7JNX.jpg) Promise中有兩個重要的參數PromiseState跟PromiseResult,也就是Promise的狀態跟值。Promise其實就是一個狀態轉移的函數,Promise有三種狀態,初始的pending、成功的fulfilled、失敗的rejected,並且只有兩種狀態改變 * pending變為fulfilled * pending變為rejected 一個Promise只會改變一次狀態,並且在狀態改變後將成功的結果value或失敗的數據Reason保存在PromiseResult中。 了解了Promise的運作機制後,接下來是Promise的一些重要API以及函式 ### util.promisify() 這是一個util模組下的函數,作用是把一個常見的錯誤優先(err, value)的回調函數重新包裝成promise風格的函數。 下面以fs.readFile()為例 ``` //Promise版 const fs = require('fs') let p = new Promise((resolve, reject)=>{ fs.readFile('./resource/content.txt', (err,data)=>{ if(err) reject(err) resolve(data) }) }) p.then(value=>{ console.log(value.toString()); }, reason=>{ console.log(reason); }) //promisify包裝 const util = require('util') const fs = require('fs') let myReadFile = util.promisify(fs.readFile) myReadFile('./content.txt').then(value=>{ console.log(value.toString()); }, reason=>{ console.log(reason); }) ``` ### Promise多任務串連 Promise物件後面能接的then函數不限一個,下面將展示then的串接用法 ``` let p = new Promise((resolve, reject) => { resolve('01') }).then(value => { console.log(value) //value = 01 return new Promise((resolve, reject) => { resolve('02') }) }).then(value => { console.log(value) //value = 02 return '03' }).then(value => { console.log(value) //value = 03 }).then(value => { console.log(value) //value = undefine }) ``` 我們可以在then中使用return,如果傳回的是一個Promise物件,那麼後面接的then就會接收該物件的結果;而如果,傳回的是一個參數或值,那麼後面的then就會直接當作接收到了成功的結果,並且結果就是傳回的值;如果我們不做任何return,那麼接收不到結果的then,裡面的value就會維持undefine的狀態 ### catch() catch的作用是抓取Promise的失敗結果,然而它的作用不只如此,它還具備異常穿透的特性,可以替一整串的Promise鏈接收失敗的回調,並終止Promise繼續向下傳遞 ``` let p = new Promise((resolve, reject) => { resolve('OK') }).then(value => { reject('err') }).then(value => { console.log('test') //不會執行 }).catch(reason => { console.log(reason) //reason = 'err' }) ``` ### Promise.all() Promise還有提供一些API,這邊我就講講我覺得比較常用的兩個,首先是all,這個函數可以接收一個Promise陣列,並且只有當所有Promise都成功時才會一次回傳所有結果,若當中有一個Promise失敗,那all函數就會提供一個失敗的回傳 ``` let p1 = new Promise((resolve, reject) => { resolve('OK') }) let p2 = new Promise((resolve, reject) => { resolve('Good') }) let p3 = new Promise((resolve, reject) => { resolve('Yes') }) const result = Promise.all([p1,p2,p3]) console.log(result) //['OK','Good',"Yes"] ``` ### Promise.race() 用法跟all差不多,不過這個函數的功用是將陣列裡的Promise中最快傳回成功結果的那個當作最終結果回傳,只會回傳一個Promise的結果 ``` let p1 = new Promise((resolve, reject) => { setTimeout(()=>{ resolve('OK') },1000) }) let p2 = new Promise((resolve, reject) => { resolve('Good') }) let p3 = new Promise((resolve, reject) => { resolve('Yes') }) const result = Promise.race([p1,p2,p3]) console.log(result) //result = 'Good' ``` ### async async函數的回傳值會是一個Promise物件,如果直接return一個值,那麼返回的就會是一個fulfilled的Promise物件 ``` async function maun(){ return new Promise((resolve, reject) => { resolve('OK') }) } let result = main() console.log(result) //'OK' ``` ### await await必須被包裹在async函數之中,它可以接收一個Promise物件並返回成功的結果,需要注意的是,它只會返回成功的結果,如果要接收錯誤的回報,則必須使用try catch函數來抓取。 ``` async function main(){ let p = new Promise((resolve, reject) => { resolve('OK') }) try{ let result = await p console.log(result) }catch(err){ console.log(err) } } main() ``` 上面已經將Promise的長用功能說了一遍,接著我們舉一個讀取檔案的例子來看看Promise如何運用以及如何解決回調地獄的問題 首先,這是使用一般回調函數的方式進行連續讀檔的程式 ``` const fs = require('fs') fs.readFile('./1.txt', (err, data1) => { if(err) throw err fs.readFile('./2.txt', (err, data2) => { if(err) throw err fs.readFile('./3.txt', (err, data3) => { if(err) throw err console.log(data1 + data2 + data3) }) }) }) ``` 隨著連續步驟越多,程式碼會不停往後縮進,造成閱讀不易 而如果善用Promise,我們可以將上述的程式修改成如下 ``` const fs = require('fs') const util = require('util') const mineReadFile = util.promisify(fs.readFile) async function main(){ try{ let data1 = await mineReadFile('./1.txt') let data2 = await mineReadFile('./2.txt') let data3 = await mineReadFile('./3.txt') console.log(data1 + data2 + data3) }catch(e){ console.log(e) } } main() ``` 程式的可讀性大幅提高了 ## 4. Fetch Fetch是獨立於XHR之外的一個函數,並不依賴於XHR,並且是ES6開始自帶的函數,只要瀏覽器支援ES6以上的版本就不需要額外引入,同時它也是一個Promise風格的函數,因此適用大部分的Promise規則。 Fetch最大的特點就是重視關注點分離,而甚麼是關注點分離呢?關注點分離的意思就是將發送請求的過程拆開,先向服務器發送請求,先確定了服務器有反饋結果,然後我們再對反饋結果解析,一般我們通過XHR發送請求都是直接看接收結果,但在fetch中多了一個步驟。 下面先舉一個fetch發送請求取得回應內容的過程 ``` fetch("http://xxxx",{method:'GET'}) .then(response => { if(!response.ok){ throw new Error(response.statusText) } return response.json() }).then(data => { console.log(data) }).catch(err => { console.log(err) }) ``` 上述範例先發送了fetch請求,返回的是一個response物件,然而需要注意的是fetch並不會幫我們過濾伺服器狀態碼,只要有接收到回應,就算是404也會進入成功的狀態,因此需要再通過response.ok來判斷狀態碼是否在200~299之間。而當確認接收到了成功的結果後,需要再根據你的資料格式通過arrayBuffer(),json(),text()等函數來取得我們要的返回資料,上述範例接收的是json物件,因此使用的是json()函數。 fetch是promise風格的函數,因此我們也可以通過async跟await把它簡化如下 ``` async function main(){ try{ const response = await fetch("http://xxxx") if(!response.ok) throw new Error(response.statusText) const data = await response.json() console.log(data) } catch(err){ console.log(err) } } main() ``` 如何,是否比xhr更加好閱讀? ## 5. axios axios也是XHR的一個封裝,相比jQuery它的包更佳輕量,並且也是Promise風格的函數,能夠使用Promise的API。除此之外,它也如同jQuery一樣具備get,post等包裝好的函數 ``` //GET請求 axios.get('http://xxxx') .then( (response) => console.log(response)) .catch( (error) => console.log(error)) //POST請求 axios.post('http://xxxx',{ id:'xxxxxxxx', password:'******' }) .then(response => console.log(response)) .catch(error => console.log(error)) //直接使用axios axios({ method: 'post', url: 'http://xxxx', data: { id:'xxxxxxxx', password:'******' } }) .then(response => console.log(response)) .catch(error => console.log(error)) ```