5-3 使用 fetch 拉取天氣觀測資料

本單元對應的專案分支為:get-current-weather-when-refresh-clicked

單元核心#

這個單元的主要目標包含:

  • 進一步了解「局屬氣象站-天氣觀測資料」API 回傳的資料內容
  • 串接「局屬氣象站-天氣觀測資料」API 回傳的資料,並顯示於畫面
  • 留意 useState 中資料為物件時,更新狀態的使用

在上一個單元中,我們先根據中央氣象局 API 回傳的資料結果,當成 currentWeather 的預設值來呈現。在這個單元中,我們將透過瀏覽器提供的 fetch API 來實際發送請求,並更新畫面上呈現的資料。

由於「臺灣好天氣」中要呈現的資料,會需要分別透過「天氣觀測」與「天氣預報」這兩支 API 取得,在這個單元先來處理「天氣觀測」的資料部分。

了解 API 回傳的天氣觀測資料#

根據先前單元,在線上說明文件試打「局屬氣象站-天氣觀測資料 API」取得回應,可以看到回應內容包含幾個部分:

Imgur

  1. success:是否成功向伺服器發送請求並取得回應
  2. result.fields:向伺服器請求的欄位資料,這裡因為在請求時沒有特別限制,預設會回傳全部欄位的資料
  3. records.location:列出所有局屬氣象站目前天氣觀測資訊,這是我們最想要的資訊
// /v1/rest/datastore/O-A0003-001 局屬氣象站-現在天氣觀測報告
{
"success": "true",
"result": {
"resource_id": "O-A0003-001",
"fields": [
// 列出所有我們請求的欄位,例如 lat, locationName, obsTime, ...等等
]
},
"records": {
// 列出各局屬氣象站的目前天氣觀測資訊
"location": []
}
}

因為 records 的部分會是我們最主要需要使用到的資料,因此我們再深入檢視一下,可以看到:

  1. locationName:局屬氣象站名稱
  2. time:觀測時間
  3. weatherElement:各天氣的觀測資料
"records": {
"location": [
// 列出各地區實際的觀測資料
{
"lat": "23.497671",
"lon": "120.424783",
"locationName": "嘉義",
"stationId": "467480",
"time": {
"obsTime": "2020-06-26 23:40:00"
},
"weatherElement": [
// 各天氣的觀測資料
],
"parameter": [
// 其他觀測站資訊
]
}
]
}

特別是在 weatherElement 屬性中提供的許多天氣資料中,可以找到我們需要用到的「溫度(TEMP)」、「風速(WDSD)」等資訊,像是:

{
"weatherElement": [
{
"elementName": "WDSD",
"elementValue": "1.10"
},
{
"elementName": "TEMP",
"elementValue": "27.90"
}
// ...
]
}

但從這些資訊中無法看出像是「降雨機率」、「氣象描述」、「白天晚上」、「晴天或降雨」等資訊,因此這個部分未來會需要再靠另一支 API 「一般天氣預報-今明 36 小時天氣預報」來補足這些部分。

info

關於 API 回應中的各欄位意義,都有描述在「局屬氣象站資料集說明檔」中:https://opendata.cwb.gov.tw/opendatadoc/DIV2/A0003-001.pdf。另外,雖然「氣象描述」的部分在文件中有提到可以透過「H_Weather」取得,但實際上會發現回傳的資料大多是 null 或是 -99,即表示沒有資料。

fetch API 的基本使用#

了解「天氣觀測」API 會回應的內容後,就可以實際撰寫一段 AJAX 來向中央氣象局拉取資料,這裡我們使用瀏覽器原生的 fetch API 來發送請求,一般使用 fetch 發送 GET 請求時,只需要在 fetch(<requestURL>) 的方法中帶入 requestURL 作為參數,這個 fetch 會是一個 Promise,因此可以透過 .then 串連伺服器回傳的資料。

程式碼會像這樣:

fetch('<requestURL>') // 向 requestURL 發送請求
.then((response) => response.json()) // 取得伺服器回傳的資料並以 JSON 解析
.then((data) => console.log('data')); // 取得解析後的 JSON 資料

因此要發送請求,只需將 requestURL 的部分換成中央氣象局提供的 API 網址就可以了。

點擊重新整理按鈕後拉取資料#

可以有幾個不同時間點來向中央氣象局請求資料,一個是在畫面載入時就自動拉取一次,另一個是在使用者點擊「重新整理」按鈕時拉取資料。現在我們先做後者,也就是使用者主動點擊的方式。

我們只需先定義好 handleClick 方法,在 handleClick 內去呼叫中央氣象局 API,接著在 <Refresh /> 按鈕綁上 onClick 事件,當事件被觸發時會呼叫 handleClick 方法,整個過程會像這樣:

  1. 先將之前取得的授權碼存成一個常數,取名為 AUTHORIZATION_KEY
// ./src/App.js
// ...
const AUTHORIZATION_KEY = '<你的授權碼>';
const App = () => {
//...
};
提示

在 JavaScript 中對於像是授權碼這類不會變更的常數,習慣以全大寫搭配底線的方式來命名。

  1. 針對某一地區發送 API 請求,這裡我們先針對台北(讀者也可以輸入其他地區)來請求當前的天氣觀測資料:
// ./src/App.js
const AUTHORIZATION_KEY = '<你的授權碼>';
const LOCATION_NAME = '臺北'; // STEP 1:定義 LOCATION_NAME
const App = () => {
// ...
// STEP 2:將 AUTHORIZATION_KEY 和 LOCATION_NAME 帶入 API 請求中
const handleClick = () => {
fetch(
`https://opendata.cwb.gov.tw/api/v1/rest/datastore/O-A0003-001?Authorization=${AUTHORIZATION_KEY}&locationName=${LOCATION_NAME}`
)
.then((response) => response.json())
.then((data) => {
console.log('data', data);
});
};
};
提醒

先前的單元中曾提到在「天氣觀測」和「天氣預報」這兩支不同的 API 中,需要使用的 locationName 不同,前者帶入的是「局屬觀測站」,例如「臺北」,後者帶入的是「縣市」,例如「臺北市」,如果帶錯的話將會無法取得正確的回應。這點在後面需要同時處理兩道 API 時會再做更多處理。

現在當 handleClick 被觸發時,就會透過 fetch 向中央氣象局發送請求。

  1. 最後我們只需要把 handleClick 這個方法透過 onClick 綁定在 <Refresh> 元件上:
// ./src/App.js
const AUTHORIZATION_KEY = '<你的授權碼>';
const App = () => {
const handleClick = () => {/* ... */}:
return (
<Container>
<WeatherCard>
{/* STEP 2:綁定 onClick 時會呼叫 handleClick 方法 */}
<Refresh onClick={handleClick}/>
</WeatherCard>
</Container>
);
};

順利的話當使用者點擊「臺灣好天氣」右下角的「重新整裡」按鈕時,就會向中央氣象局發送請求,並取得資料,你將可以在瀏覽器的 console 視窗中看到回傳的資料內容:

Imgur

更新元件內的資料狀態#

現在已經可以在使用者點擊按鈕後,向中央氣象局發送請求並取得回應,但是因為還沒被把這些資料內容帶回到 React 元件中,因此畫面並不會改變,這時候你可能已經想到了,要在改變資料的時候同時讓畫面重新轉譯(render),就可以用 useState() 中回傳給我們的 setCurrentWeather 這個方法。

從 API 回傳的資料來看,我們需要的資料會在 records.location 這個陣列中元素的 weatherElement 屬性中,當中最重要的會是溫度(TEMP)和風速(WDSD):

Imgur

所以在使用 setCurrentWeather 來把這些資料帶回元件中時,需要先把用得到的資料取出來。

先稍微說明一下這裡的邏輯:

  1. 定義 locationData 把回傳的資料中會用到的部分取出來
// STEP 1:定義 `locationData` 把回傳的資料中會用到的部分取出來
const locationData = data.records.location[0];
  1. 因為風速(WDSD)、氣溫(TEMP)這些資料都存在 locationData.weatherElement 中,這裡透過陣列的 reduce 方法搭配 includes 可以把需要的資料取出來
// STEP 2:將風速(WDSD)和氣溫(TEMP)的資料取出
const weatherElements = locationData.weatherElement.reduce(
(neededElements, item) => {
if (['WDSD', 'TEMP'].includes(item.elementName)) {
neededElements[item.elementName] = item.elementValue;
}
return neededElements;
},
{}
);

這裡透過 reduce 組合出來的 weatherElements 將會長這樣:

// weatherElements
{
WDSD: 1.10,
TEMP: 33.20
}
  1. 在取得所需要的資料後(除了descriptionrainPossibility 的部分需要再透過額外的 API 取得),就可以透過 useState 回傳的 setCurrentWeather 方法來更新 React 內的資料狀態:
// STEP 3: 更新 React 元件中的資料狀態
setCurrentWeather({
observationTime: locationData.time.obsTime,
locationName: locationData.locationName,
temperature: weatherElements.TEMP,
windSpeed: weatherElements.WDSD,
description: '多雲時晴',
rainPossibility: 60,
});

最後就把上面寫好的邏輯放到原本的 handleClick 方法中:

// ./src/App.js
const [currentWeather, setCurrentWeather] = useState({
/* ... */
});
const handleClick = () => {
fetch(/* 中央氣象局 API */)
.then((response) => response.json())
.then((data) => {
const locationData = data.records.location[0]; // STEP 1:取出資料
const weatherElements = locationData.weatherElement.reduce(/* ... */); // STEP 2:過濾資料
// STEP 3:更新 React 資料狀態
setCurrentWeather({
observationTime: locationData.time.obsTime,
// ...
});
});
};

現在當我們點擊「臺灣好天氣」右下角的重新整理按鈕時,就可以看到當前最新的資料狀態(除了天氣描述和降雨機率)!

useState 中帶入物件時須留意的地方#

前幾個章節中,在實作計數器和網速單位換算器時,useState 的裡面的值都是放入數值,但除了基本的數值、字串、布林之外,保存在 React 元件內的資料狀態也可以是物件或陣列。像是在上面 currentWeather 中我們就是放入物件:

const [currentWeather, setCurrentWeather] = useState({
observationTime: '2020-12-12 22:10:00',
locationName: '臺北市',
description: '多雲時晴',
windSpeed: 3.6,
temperature: 32.1,
rainPossibility: 60,
});

setSomething 會把舊有的資料完全覆蓋#

但要特別留意的是,當我們使用物件時,如果有需要保留物件中原有的屬性時,不能只是在 setCurrentWeather 帶入想要變更的物件屬性,因為 setSomething 這種用法會完全以傳入的值覆蓋掉舊有的內容。

什麼意思呢?假設現在我只想要修改 currentWeathertemperature 的值,其他屬性想要保留不變的話,我們不能這樣寫:

// ❌ 錯誤:不能只寫出要修改或添加的物件屬性
setCurrentWeather({
temperature: 31,
});
console.log(currentWeather); // { temperature: 31}

因為 setSomething 這種方法會用新給的資料全部覆蓋掉舊有的資料,因此 currentWeather 會變成只剩下 temperature 這個屬性。

正確的做法應該要把舊有的資料透過物件的解構賦值帶入新物件中,再去添加或修改想要變更的屬性,像是這樣:

// ✅ 正確:先透過解構賦值把舊資料帶入新物件中,再去添加或修改想要變更的資料
setCurrentWeather({
...currentWeather,
temperature: 31,
});

如此更新後的 currentWeather,才會是先保留了原本的 currentWeather 中的所有屬性後,接著才更新 temperature 屬性的值,而不會變成只剩下 temperature 屬性而已。

要使用多次 useState 還是把所有資料都包在一個物件中只使用一次#

一般來說,在一個 React Component 中多次呼叫 useState 並不會有太大的問題,因此不建議單純只是為了想要少用幾次 useState 而把所有不相關的資料都放到同一個物件中,因為這代表你將只會得到一個 setSomething 的方法,而你只要呼叫到這個方法,因為是用新的資料整個覆蓋掉舊的,因此即使有很多不需要更新的資料,但仍會被迫整個換掉。

因此官方建議,可以將有關聯的資料放在同一個物件中,而沒有關聯的資料,就另外再使用 useState 去定義資料狀態。

換你了!向中央氣象局請求真實的觀測資料#

現在,換你實際透過 AJAX 的方式,向中央氣象局請求真實的資料回來呈現吧!你可以參考下述的步驟:

  • 了解「局屬氣象站-現在天氣觀測報告」API 中會回傳的資料內容
  • 撰寫 handleClick 方法,並將該方法綁定在 <Refresh /> 按鈕的 onClick 事件上
  • 定義在 AJAX 請求中會使用到的常數,包含 AUTHORIZATION_KEYLOCATION_NAME
  • 當使用者點擊重新整理的按鈕後,透過 fetch 方法向中央氣象局 API 發送請求,並取得回應
  • 檢視回應的資料內容,並過濾出我們所需要的資料
  • 透過 setCurrentWeather 方法來更新 React 元件中的資料狀態
  • 確定畫面有因為資料變更而連動更新,顯示當前的天氣資料

本單元相關之網頁連結、完整程式碼與程式碼變更部分可於 get-current-weather-when-refresh-clicked 分支檢視:https://github.com/pjchender/learn-react-from-hook-realtime-weather-app/tree/get-current-weather-when-refresh-clicked

Imgur