感謝 iT 邦幫忙與博碩文化,本系列文章已出版成書「從 Hooks 開始,讓你的網頁 React 起來」,首刷版稅將全額贊助 iT 邦幫忙鐵人賽,歡迎前往購書,鼓勵筆者撰寫更多優質文章。
useMemo
昨天終於處理掉了即時天氣 App 中最軟的那塊,也就是天氣圖示的部分,就再鬆了一口氣的時候,腦中突然浮出公司設計師發出的聲音...,祂說:「別忘了白天和晚上要使用不同的天氣圖示,白天要用有太陽了,晚上要用有月亮了」。
果然設計師就是比較細心,還有考慮到白天和晚上的問題。好在一開始在找圖示的時候,就已經找好有白天(太陽)和晚上(月亮)的圖示,同時昨天定義的 weatherIcons
這個變數中也考慮好了白天和晚上要使用不同的圖示,所以改起來不會這麼複雜。
既然要根據天亮和天黑來使用不同的天氣圖示,就需要先判斷什麼時候是「天亮」什麼時候是「天黑」;既然設計師都這麼用心的考慮到了這種情境,就不能只是自己定個早上六點開始算天亮、晚上六點之後算天黑這麼草率。
於是,責無旁貸的我決定來找找看中央氣象局有沒有提供各地區日出和日落的 API,發現中央氣象局雖然沒有提供查詢日出日落的 API,但是在「資料主題」的「天文」中,有提供「日出日沒時刻-全臺各縣市年度逐日日出日沒時刻資料」的 JSON 檔可以下載:
下載後會發現這包檔案資料還蠻大的,這個 JSON 物件中我們需要用到的日出日落資料主要在 dataset.locations.location
中:
location
這個陣列中包含台灣各地區日出和日落的時間,而某個陣列元素中(例如,新北市)的 time
欄位會包含從 2019-01-01
到 2020-12-31
的日出日落時間:
再來點進去 parameter
後會發現,它不只列出了「日出」、「日落」時間,還列出「民用曙光」、「方位」、「仰角」...等資訊。
總之,天文果然不是我們想的這麼單純,宇宙的奧秘只有立志征服宇宙的男人/女人才能達到啊...。
這麼大一包資料中有許多是在即時天氣 App 中是用不到的,因此可以先來過濾出需要用到的資料,過濾的條件如下:
dataTime
的欄位超過今天(2019-10-08)parameter
只保留「日出時刻」和「日落時刻」因為資料整理的過程並非這篇的重點,為了避免混淆會把程式碼放到 repl.it 上 filterSunriseAndSunsetData,有興趣的朋友可以再去檢視。
整理後的資料可以到 Dropbox 或 Gist 下載,資料格式改成第一層會顯示地區(locationName
),找到地區之後在 time
裡面會列出每天日出(sunrise
)和日落(sunset
)的時間:
最後,就可以根據日出或日落的時間來判斷要顯示太陽或月亮。如果你直接略過上一段的話,記得先把處理後的日出日落檔案從 Dropbox 或 Gist 下載,並且上傳一份到 CodeSandbox 上。由於這份檔案的內容相當多,所以上傳後 CodeSandbox 可能只會回傳一個 JSON 檔案的網址給你:
這時候你可能要等他一下,一陣子之後,它才會把這個檔案的內容載入。
在取得了日出和日落的時間後,就可以撰寫一個函式來判斷現在的時間到底是白天還是晚上,我們把這個函式命名為 getMoment
,這個函式可以帶入地區 locationName
當作參數:
import
把剛剛上傳的日出日落資料載入null
new Date()
取得當前時間2019-10-08
,因此把當前時間也轉成這種時間格式getTime()
這個方法轉成時間戳記(TimeStamp)day
),否則為晚上(night
)// STEP 1:匯入日出日落資料
import sunriseAndSunsetData from './sunrise-sunset.json';
// ...
const getMoment = (locationName) => {
// STEP 2:從日出日落時間中找出符合的地區
const location = sunriseAndSunsetData.find(
(data) => data.locationName === locationName
);
// STEP 3:找不到的話則回傳 null
if (!location) return null;
// STEP 4:取得當前時間
const now = new Date();
// STEP 5:將當前時間以 "2019-10-08" 的時間格式呈現
const nowDate = Intl.DateTimeFormat('zh-TW', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
})
.format(now)
.replace(/\//g, '-');
// STEP 6:從該地區中找到對應的日期
const locationDate =
location.time && location.time.find((time) => time.dataTime === nowDate);
// STEP 7:將日出日落以及當前時間轉成時間戳記(TimeStamp)
const sunriseTimestamp = new Date(
`${locationDate.dataTime} ${locationDate.sunrise}`
).getTime();
const sunsetTimestamp = new Date(
`${locationDate.dataTime} ${locationDate.sunset}`
).getTime();
const nowTimeStamp = now.getTime();
// STEP 8:若當前時間介於日出和日落中間,則表示為白天,否則為晚上
return sunriseTimestamp <= nowTimeStamp && nowTimeStamp <= sunsetTimestamp
? 'day'
: 'night';
};
透過上面這個函式,就可以取得當前時間是白天或是晚上了。
可以看出上面這個也是個複雜的運算,因此除非有特定資料改變,否則不希望每次畫面重新渲染時都要在重新計算一次,沒錯,這裡又可以應用到昨天學到的 useMemo
,就直接套到組件中吧:
useMemo
getMoment
函式放進來WeatherApp
組件中使用 useMemo
,裡面就去呼叫 getMoment
方法取得回傳值,並且帶入 dependenciesuseMemo
取得的回傳值透過 props 帶入 <WeatherIcon />
組件中,這裡因為當一開始還沒透過 API 拉取到資料時,moment
會是 null
,因此可以透過 ||
帶入預設值為 day
// STEP 2:匯入 useMemo
import React, { useState, useEffect, useCallback, useMemo } from 'react';
// ...
// STEP 1:定義 getMoment 方法
const getMoment = (locationName) => {
const location = sunriseAndSunsetData.find(
(data) => data.locationName === locationName
);
if (!location) return null;
const now = new Date();
const nowDate = Intl.DateTimeFormat('zh-TW', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
})
.format(now)
.replace(/\//g, '-');
const locationDate =
location.time && location.time.find((time) => time.dataTime === nowDate);
const sunriseTimestamp = new Date(
`${locationDate.dataTime} ${locationDate.sunrise}`
).getTime();
const sunsetTimestamp = new Date(
`${locationDate.dataTime} ${locationDate.sunset}`
).getTime();
const nowTimeStamp = now.getTime();
return sunriseTimestamp <= nowTimeStamp && nowTimeStamp <= sunsetTimestamp
? 'day'
: 'night';
};
// ...
const WeatherApp = () => {
// ...
// STEP 3:透過 useMemo 避免每次都須重新計算取值,記得帶入 dependencies
const moment = useMemo(() => getMoment(weatherElement.locationName), [
weatherElement.locationName,
]);
// ...
return (
<Container>
<WeatherCard>
{/* ... */}
<CurrentWeather>
<Temperature>
{Math.round(weatherElement.temperature)} <Celsius>°C</Celsius>
</Temperature>
{/* STEP 4:將 moment 帶入 props 中 */}
<WeatherIcon
currentWeatherCode={weatherElement.weatherCode}
moment={moment || 'day'}
/>
</CurrentWeather>
{/* ... */}
</WeatherCard>
</Container>
);
};
export default WeatherApp;
現在即時天氣 App 的天氣圖示就可以在白天的時候出現太陽,在晚上的時候出現月亮了!
經過了這兩天,你有沒有發現有時候資料的處理才是真正花時間的地方,特別是在於前後端無法溝通,前端不能直接和後端要求回傳的資料格式時??
今天完整的程式碼一樣可以在 CodeSandbox 上檢視:Weather APP - dynamic weather icon with sunrise and sunset data。
請問toLocaleDateString()用法可行嗎
const nowDate = now.toLocaleDateString().replace(///g, '-')
我這裡用 now.toLocaleDateString().replace(///g, '-')
的話,得到的結果會是 3-13-2020
,這樣可能會沒辦法匹配到資料表裡時間,因為在日出日落的資料中,時間格式是 2020-03-13
這種寫法。
另外,關於因為在 Safari 中無法正確解析資料表中的時間格式,所以後來解析時間的部分,我使用了 dayjs,有興趣的話可以參考修改的這個 commit (fix: wrong sunset and sunrise time on Safari) 喔!
謝謝!
我開啟你程式碼的連結去運行程式時,程式出現錯誤。
"TypeError
Cannot read properties of undefined (reading 'dataTime')".
但我看網頁上的程式碼是對的,沒有問題。
我試了另一部電腦,都是出現相同錯誤。但是,我在自己codesandbox上運行就沒有錯誤。
看起來是 sunset-sunrise.json 中的日出日落時間過期了,已經更新好了,再麻煩檢視看看~
看了沒有問題。
你好,因為 getMoment 裡面有參考目前的時間,所以感覺不適合放在 useMemo,如果設一個計時器的 useState 每五分鐘改變一次值,然後讓 useMemo 同時監聽計時器還有地點的變化感覺比較好。
function weatherApp() {
[timer, setTimer] = useSate(new Date().getTime())
if (new Date().getTime() - timer > 5 * 60 * 1000) {
setTimer(new Date().getTime())
}
const moment = useMemo(() => getMoment(weatherElement.locationName), [
weatherElement.locationName, timer
]);
}
很好的理解和嘗試,只要 component rerender 的時就會重現檢查一次時間,但如果沒有 rerender 的話,它不會自己檢查的,還是可以看你的使用需求和情境來調整,有需要的話可以在搭配其他 timer 使用。