constgetCountryData=function(country){constrequest=newXMLHttpRequest()// open request
request.open('GET',`https://restcountries.com/v2/name/${country}`)request.send()// send request to url, fetch in the background
// can not use data = request.response to get result because not finished
console.log(request.responseText)// can not immediately get anything
// once it done it will emit load event
// load after it is done
request.addEventListener('load',function(){console.log(this.responseText)// this = request
// will get [{...}], so destructure like this
const[data]=JSON.parse(this.responseText)})// 根據收到資料update
constcountriesContainer=document.querySelector('.countries')consthtml=`
<article class="country">
<img class="country__img" src="${data.flag}" />
<div class="country__data">
<h3 class="country__name">${data.name}</h3>
<h4 class="country__region">${data.region}</h4>
// 四捨五入到百萬
<p class="country__row"><span>👫</span>${(Number(data.population)/1000000).toFixed(1)}</p>
<p class="country__row"><span>🗣️</span>${data.languages[0].name}</p>
<p class="country__row"><span>💰</span>${data.currencies[0].name}</p>
</div>
</article>
`countriesContainer.insertAdjacentHTML('beforeend',html)}getCountryData('portugal')getCountryData('usa')
// 根據收到資料update
constcountriesContainer=document.querySelector('.countries')constrenderCountry=function(data,className=''){consthtml=`
<article class="country ${className}">
<img class="country__img" src="${data.flag}" />
<div class="country__data">
<h3 class="country__name">${data.name}</h3>
<h4 class="country__region">${data.region}</h4>
// 四捨五入到百萬
<p class="country__row"><span>👫</span>${(Number(data.population)/1000000).toFixed(1)}</p>
<p class="country__row"><span>🗣️</span>${data.languages[0].name}</p>
<p class="country__row"><span>💰</span>${data.currencies[0].name}</p>
</div>
</article>
`countriesContainer.insertAdjacentHTML('beforeend',html)countriesContainer.style.opacity=1}constgetCountryAndNeighbor=function(country){// AJAX call country 1
constrequest=newXMLHttpRequest()// open request
request.open('GET',`https://restcountries.com/v2/name/${country}`)request.send()// load after it is done
request.addEventListener('load',function(){// array
const[data]=JSON.parse(this.responseText)// [ {...} ]
renderCountry(data)// get neighbour country (2)
const[neighbour]=data.bordersif(!neighbour)=return// AJAX call country 2
constrequest2=newXMLHttpRequest()// open request
request2.open('GET',`https://restcountries.com/v3.1/alpha/${neightbor}`)request2.send()request2.addEventListener('load',function(){// obj
constdata2=JSON.parse(this.responseText)renderCountry(data2,'neighbour')})})}
getCountryAndNeighbor(‘usa’)
Why Promises with fetch
Promise 類似一個接收未來非同步函式回傳結果的容器
不用依賴 events 跟 callback 傳遞跟處理非同步函式結果
透過 chain Promise 可以避免 callback hell,打造更好讀的程式碼
Promise 的生命週期
fetch API in a Promise
pending (response not available yet)
async task settled (get results)
fulfilled
rejected
consume Promise
Promise 結構
fetch 後會回傳一個 Promise
透過 json()方法解析後,會再回傳一個 Promise
透過 then 接起來,獲取 data
then 區塊回傳 fulfilled value
catch 區塊 回傳 rejected value,chain 中所有的錯誤會在這裡被拋出
err 是一個 object,可以把使用者需要知道的錯誤顯示在上面
finally 區塊是無論回傳結果永遠都會執行的區塊,處理一定需要執行的邏輯
用 then 來 chain Promise,不要把 then chain 成巢狀,在外層接 Promise
callback v.s. Promise
1
2
3
4
5
6
7
8
9
10
11
12
13
// XMLHttpRequest
constrequest=newXMLHttpRequest()request.open('method','url')request.send()request.addEventListener('load',function(){constdata=JSON.parse(this.responseText)renderCountry(data,'neighbour')})// Promise with less code
constrequest=fetch('url').then((response)=>response.json()).then((data)=>renderCountry(data,'neighbour'))
constgetCountryData=function(country){fetch(`https://restcountries.com/v2/name/${country}`).then(response=>{console.log(response)// 第一個錯誤處理 status = 404, ok =false
if(!response.ok)thrownewError(`Country not found (${response.status})`)// 沒有這個msg 就會render出來'flag' of undefined
returnresponse.json()}).then(data=>{renderCountry(data[0])// const neighbour = data[0].borders[0]
// 用不存在的國家模擬network無法抓取資料狀況
constneighbour='123123'if(!neighbour)return// 請求會被reject
returnfetch(`https://restcountries.com/v2/name/${neighbour}`)}).then(response=>{if(!response.ok){thrownewError(`Country not found (${response.status})`)}returnresponse.json()}).then(data=>renderCountry(data,'neighbour')).catch(err=>{console.log(err)renderError(`something is wrong ${err.message}`)}).finally(()=>{countriesContainer.style.opacity=1})}
重複 fetch 的 code 可以精簡成一個函式
1
2
3
4
5
6
7
8
9
constgetJSON=function(url,errMsg='something went wrong'){fetch(url).then((response)=>{if(!response.ok){thrownewError(`${errorMsg}${response.status}`)returnresponse.json()}})}
constgetCountryData=function(country){getJSON(`https://restcountries.com/v2/name/${country}`,'Country not found').then(data=>{renderCountry(data[0])constneighbour=data[0].borders[0]// 沒有鄰國的狀況也丟錯誤
if(!neighbour){thrownewError('no neighbour found!')}returngetJSON(`
https://restcountries.com/v2/name/${neighbour}`,'Country not found')}).then(data=>renderCountry(data,'neighbour')).catch(err=>{console.log(err)renderError(`${err.message}`)}).finally(()=>{countriesContainer.style.opacity=1})}
constwhereAmI=function(lat,lng){fetch(`https://geocode.xyz/${lat},${lng}?geoit=json`).then((res)=>{if(!res.ok)thrownewError(`Problem with geocoding ${res.status}`)returnres.json()}).then((data)=>{console.log(`You are in ${data.city}, ${data.country}`)returnfetch(`https://restcountries.eu/rest/v2/name/${data.country}`)}).then((res)=>{if(!res.ok)thrownewError(`Country not found (${res.status})`)returnres.json()}).then((data)=>renderCountry(data[0])).catch((err)=>console.error(`${err.message}`))}whereAmI(52.508,13.381)whereAmI(19.037,72.873)
constgetPosition=function(){returnnewPromise(function(resolve,reject){navigator.geolocation.getCurrentPosition(resolve,reject)})}constwhereAmI=function(){getPosition().then((pos)=>{const{latitude:lat,longitude:lng}=pos.coordsreturnfetch(`https://geocode.xyz/${lat},${lng}?geoit=json`)}).then((res)=>{if(!res.ok)thrownewError(`Problem with geocoding ${res.status}`)returnres.json()}).then((data)=>{console.log(data)console.log(`You are in ${data.city}, ${data.country}`)returnfetch(`https://restcountries.eu/rest/v2/name/${data.country}`)}).then((res)=>{if(!res.ok)thrownewError(`Country not found (${res.status})`)returnres.json()}).then((data)=>renderCountry(data[0])).catch((err)=>console.error(`${err.message}`))}btn.addEventListener('click',whereAmI)
打造 2 秒換圖功能
1
2
3
4
<!-- html --><mainclass="container"><divclass="images"></div></main>
constget3Countries=asyncfunction(country1,country2,country3){try{// 這樣寫太冗,所以後來有了Promise.all
// const [data1] = await getJSON(
// `https://restcountries.com/v2/name/${country1}`, 'data not found')
// const [data2] = await getJSON(
// `https://restcountries.com/v2/name/${country2}`, 'data not found')
// const [data3] = await getJSON(
// `https://restcountries.com/v2/name/${country3}`, 'data not found')
constdata=awaitPromise.all([getJSON(`https://restcountries.com/v2/name/${country1}`,'data not found'),getJSON(`https://restcountries.com/v2/name/${country3}`,'data not found'),getJSON(`https://restcountries.com/v2/name/${country2}`,'data not found')])console.log(data.map(country=>country[0].capital)}catch(err){console.error(err)}}get3Countries('portugal','canada','tanzania')
Promise.race
陣列中只會出現最先完成的結果(可能是成功或拒絕)
當使用者網路不好,等待時間太久可以用這個 reject
1
2
3
4
5
6
7
8
;(asyncfunction(){constres=awaitPromise.race([getJSON(`https://restcountries.com/v2/name/italy}`,'data not found'),getJSON(`https://restcountries.com/v2/name/egypt`,'data not found'),])console.log(res[0])})()// 只會是italy or egypt其中一個內容
1
2
3
4
5
6
7
8
9
10
11
12
// 計時器,超過5秒就發出拒絕
consttimeout=function(second){returnnewPromise(function(_,reject){setTimeout(function(){reject(newError('Request too long'))},second*1000)})}Promise.race([getJSON(`https://restcountries.com/v2/name/italy`),timeout(5)]).then((res)=>console.log(res[0])).catch((err)=>console.log(err))