6-3 根據白天或夜晚顯示不同的主題配色

本單元對應的專案分支為:get-moment-from-sunrise-sunset

單元核心#

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

  • 了解判斷當前為白天或夜晚的邏輯
  • 根據白天或夜晚來改變亮/暗主題配色

一開始時的天氣圖示就分成了白天和夜晚這兩種,但因為還沒進行白天或夜晚的判斷,所以先前 WeatherIcon 元件的 props 都直接使用 moment="night" 作為預設值帶入。

但既然要根據天亮和天黑來使用不同的天氣圖示,就需要先判斷什麼時候是「天亮」什麼時候是「天黑」。實作上,比較簡單的做法會是自己定個早上六點開始算天亮、晚上六點之後算天黑;或者透過也可以透過第三方 API 的方式來查詢當前時間是屬於白天或晚上。

判斷日出與日落的邏輯#

既然要做就做得精準一些,我們就來使用各縣市「日出或日落」的時間作為白天和夜晚判斷的依據。

取得各縣市日出日落的時間#

雖然中央氣象局沒有提供查詢日出日落的 API,但是在「資料主題」的「天文」中,有提供全臺各縣市每天日出日落的資料可以下載。為了避免讀者偏離 React 的學習,而都在進行資料操作,筆者將這份日出日落的資料,整理成本專案可以使用的日出日落檔案後,直接供讀者使用,若對於實際上日出日落資料的處理有興趣的話,可以在參考本單元 Github 專案分支上的說明文件。

這份整理好的日出日落檔案,在一開建立專案時就已經請讀者們下載到 ./src 中的 utils 資料夾內,檔案名稱為 sunrise-sunset.json,這份檔案的內容如下:

  • 在外層陣列中,會列出台灣的每一個縣市(locationName
  • 每一個縣市的 time 屬性中,又會列出每天(dataTime)的日出(sunrise)與日落(sunset) 時間,以這份檔案所列的時間來說,可以沿用到 2022 年結束
// ./src/utils/sunrise-sunset.json
[
{
"locationName": "臺北市",
"time": [
{
"dataTime": "2020-07-04",
"sunrise": "05:09",
"sunset": "18:48"
}
// ...
]
},
{
"locationName": "新北市",
"time": [
{
"dataTime": "2020-07-04",
"sunrise": "05:09",
"sunset": "18:48"
}
]
}
]
info

中央氣象局會持續更新這份日出與日落資料,未來超過 2022 年後,可以下載最新的日出日落檔案進行更新,或參考本單元專案分支在 Github 說明頁所提供的更新方式。

判斷目前是白天或夜晚#

有了台灣各縣市日出日落的資料後,現在要撰寫一個函式來判斷使用者在操作 App 時的時間是白天還是晚上,我們把這個函式命名為 getMoment,這個函式可以帶入縣市名稱 locationName 當作參數,並回傳當前時間是屬於白天(day)或晚上(night),像是這樣:

// ./src/utils/helpers.js
const getMoment = (locationName) => {
// ...
return sunriseTimestamp <= nowTimeStamp && nowTimeStamp <= sunsetTimestamp
? 'day'
: 'night';
};
info

需要稍微留意一下, getMoment 函式的參數 locationName 使用的是和「天氣預報」一樣的縣市名稱,而非「天氣觀測」使用的局屬氣象站名稱。

由於 getMoment 這個函式主要是進行資料處理,和 React 的學習上沒有直接的關係,因此這裡不會對此函式內容做太多說明,要請讀者先到 ./src/utils/ 資料夾中,新增一支名為 helpers.js 的檔案,並把下方網址的程式內容,複製貼上到 src/utils/helpers.js 中:

https://github.com/pjchender/learn-react-from-hook-realtime-weather-app/blob/get-moment-from-sunrise-sunset/src/utils/helpers.js

Imgur

getMoment 函式的內容,複製貼上到 ./src/utils/helpers.js 這支檔案中:

Imgur

讀者目前只需要知道透過 getMoment 這個函式,輸入地點(locationName)即可得到當前該地區是屬於白天(day)或夜晚(night),在該檔案中針對各個步驟有附上註解說明,有興趣的朋友們可以再自行檢視資料處理的方式。

現在,有了日出日落的資料以及 getMoment 函式後,就可以根據使用者操作此 App 的時間,來判斷當時的時間是屬於白天或晚上。

於元件中取得當前是白天或晚上#

上面不論是 sunrise-sunset.json 的產生,或者是 getMoment 函式的撰寫,目的都是讓我們能立即取得某一地區現在是屬於白天或晚上。現在我們就直接根據已經寫好的 getMoment 方法取得 moment 後在元件中使用。

打開 ./src/App.js

  1. 在最上方的地方透過 import 匯入剛剛寫好的 getMoment 方法:
// ./src/App.js
import { getMoment } from './utils/helpers';
// ...
  1. getMoment 這個函式只需要帶入 locationName 後即會回傳當前是白天(day)或晚上(night)。現在讓我們在 App 元件中試著用用看 getMoment 這個方法:
// ./src/App.js
import { getMoment } from './utils/helpers';
// ...
const LOCATION_NAME_FORECAST = '臺北市';
const App = () => {
// ...
const [weatherElement, setWeatherElement] = useState(/* ... */);
const moment = getMoment(LOCATION_NAME_FORECAST);
// ...
};
  1. 最後把取得的 moment 帶入 App 元件中回傳的 JSX 中,也就是原本 WeatherIcon 的位置:
{
/* 把 moment 的值改成真是資料 */
}
<WeatherIcon weatherCode={weatherCode} moment={moment} />;

現在回到頁面,你應該會發現,現在天氣圖示已經可以根據地區和日出日落的時間來判斷是白天或夜晚了!

info

這裡我們仍是把地區寫成常數「臺北市」,在後續的單元中,會再讓使用者調整設定的所在地區。

使用 useMemo 進行效能優化#

和上一個單元相同,這裡 getMoment 因為要從許多的資料中去匹配地區和時間,算是比較需要耗費資源的運算,因此同樣的可以用 useMemo 這個 React Hook,只要在 locationName 沒有變更的情況下,就不需要重新運算。

現在因為我們還沒實作讓使用者可以自行切換縣市的功能,因次地區會先用常數 LOCATION_NAME_FORECAST 的「臺北市」表示,但依然可以先透過 useMemo 來處理這個部分,待後續使用者可以更改 locationName 時再進行 useMemo 的 dependencies 陣列去做對應的修改即可:

import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { getMoment } from './utils/helpers';
// ...
// TODO: 等使用者可以修改地區時要修改裡面的參數,先將 dependencies array 設為空陣列
const moment = useMemo(() => getMoment(LOCATION_NAME_FORECAST), []);
tip

這裡因為 LOCATION_NAME_FORECAST 是固定不會變的常數,因此就算把它放到 dependencies 陣列中,也和把它設為空陣列 [] 效果是相同的。因此,這裡先留個 TODO,待後面單元中讓使用者可以自行修改地區時,再來對 dependencies 陣列進行調整。

根據日出日落調整主題配色#

還記得在建立「臺灣好天氣」UI 畫面的時,已經實作了深色主題,但當時因為沒有額外的資訊,因此預設使用亮色主題(light)。現在既然我們知道使用者現在是白天或晚上,就可以根據這個資訊來判斷要使用亮色或暗色主題。

在一個元件中可以根據需要使用多個 useEffect,不必把所有邏輯都寫一個 useEffect 內,比較好的方式是根據程式邏輯或 dependencies 陣列會相異到的變數來建立多個不同的 useEffect。這裡我們新增一個 useEffect 來幫設定主題配色,並且透過 dependencies 陣列的使用,只有當 moment 有改變時,才會再次執行樣式主題的更換:

  • moment 是白天(day) 時,套用亮色(light)的主題配色
  • moment 不是白天(day)時,則套用暗色(dark)的主題配色
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { getMoment } from './utils/helpers';
// ...
const App = () => {
// ...
const moment = useMemo(() => getMoment(LOCATION_NAME_FORECAST), []);
useEffect(() => {
// 根據 moment 決定要使用亮色或暗色主題
setCurrentTheme(moment === 'day' ? 'light' : 'dark');
}, [moment]); // 記得把 moment 放入 dependencies 中
// ...
};

換你了!取得使用者地區是白天或晚上#

在這個單元中,我們進行了許多和 React 沒有這麼直接相關的運算,你會發現前端除了畫面與互動外,有很多時間是在處理透過 API 取得的資料。現在要請你利用已經寫好的 getMoment 函式,讓天氣圖示能夠根據白天或夜晚來顯示出太陽或月亮,同時還要能夠自動調整對應的主題配色。

你可以參考下面的步驟:

  • ./src/utils 資料夾中,新增一支 helpers.js
  • 將本單元於 Github 分支上的 ./src/utils/helpers 檔案,複製貼上到本機剛建立的 helpers.js
  • 在 App 元件中透過 import 匯入 getMoment 這個方法
  • getMoment 方法中帶入縣市名稱(locationName),取得 App 操作時是屬於白天(day)或晚上(night),並將結果取名為 moment
  • 將取得的 moment 透過 props 傳入 WeatherIcon 元件中
  • 使用 useMemo 優化效能,避免不必要的重新運算
  • 使用 useEffect 與 setCurrentTheme 來讓主題配色能根據當時的 moment 來自動切換亮/暗色主題

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

Imgur