大家好,我是若川(點這里加我微信?ruochuan12,長期交流學習)。今天推薦一個練手的React項目,創建天氣應用,相信很快能看完。昨天發送書掉粉18人,是我沒想到的,送書一般是出版社按閱讀量贊助幾本,一般也會送一本給號主。感謝屏幕前的里一直關注著我。
點擊下方卡片關注我、加個星標,或者查看源碼等系列文章。學習源碼整體架構系列、年度總結、JS基礎系列
React 是一個很棒的前端框架,可以用來構建用戶界面。
使用 React 的優勢之一是:我們創建的組件會被封裝起來,換句話說,組件是不可見的。
在本文中,我們通過構建一個天氣應用來學習 React。
如何安裝 Node 和 npm
為了構建 React 應用,我們需要安裝 Node 運行時環境,它主要是用來執行 JavaScript 代碼。
在?https://nodejs.org/en/?下載安裝包。
還需要安裝 npm,它是用 Node 構建的一個包管理器,可以在 JavaScript 應用中用它來安裝依賴包。幸運的是,npm 包含在 Node 中,所以不必另外安裝它。
安裝完成后,打開終端(terminal)或命令提示符(command prompt),輸入 node -v
命令,查看當前系統中的 Node 版本。
如何創建 React 應用
要創建 react 應用,可以在終端輸入 npx create-react-app <Your app name>
命令(此例中命令為 npx create-react-app my-weather-app
)。
可以看到相關依賴包正在自動安裝。
依賴包安裝完成之后,進入項目根目錄(通過 cd
命令),執行 npm start
命令。
可以看到默認的 React 模板頁面:
默認的 React 代碼模板:
App.js
我們不需要這些,所以要清理一些無用代碼。
在 app.js 中,刪除 div
標簽內的內容,刪除導入 logo 的代碼。
完成之后,頁面內容會變成空白。
清理之后的 app.js
如何安裝我們需要的依賴包
為了讓這個應用更有吸引力,我們需要安裝一些外部依賴包。
我們需要用到 Semantic React UI 庫,執行以下命令來安裝:
npm?install?semantic-ui-react?semantic-ui-css
安裝完成后,在 index.js 中引入這個庫,將以下代碼復制到 index.js 中即可:
import?'semantic-ui-css/semantic.min.css'
還需要安裝 moment.js 來格式化時間,執行以下命令:
npm?install?moment?--save
可以查看 package.json 文件來了解安裝了哪些包:
package.json
在這里,可以看到目前安裝的所有依賴包。
如何創建我們的天氣應用
為了讓我們的天氣應用正常工作,需要使用 OpenWeatherMap 提供的 API 來獲取天氣信息。
訪問 https://home.openweathermap.org/users/sign_up 頁面,創建自己的賬號。
完成之后,點擊導航欄上的 API 選項,可以看到不同類型的天氣數據,如當前天氣、未來 4 天逐小時預報、未來 16 天預報等。這些就是用來獲取天氣信息的 API 端點。
調用這些 API 需要用到 API key,點擊右上角的用戶名,接著點擊 “My API Keys”,可以查看自己的 API key。
如果沒有的話就創建一個。
在項目根目錄新建一個 .env 文件。
這是一個環境變量文件,保存所有 API 端點和 key 信息。
REACT_APP_API_URL?=?'https://api.openweathermap.org/data/2.5'
REACT_APP_API_KEY?=?【把你的?API?key?粘貼在這里】
REACT_APP_ICON_URL?=?'https://openweathermap.org/img/w'
把你的 API key 粘貼在 REACT_APP_API_KEY 變量中。
如何使用 React Hooks
React Hooks 讓我們能夠在函數組件中使用、管理 state。
我們會用到 useState
和 useEffect
兩個 hook,在頂部引入它們:
import?React,?{?useEffect,?useState?}?from?"react";
創建兩個 state,一個是緯度(lat),一個是經度(long)。
const?[lat,?setLat]?=?useState([]);
const?[long,?setLong]?=?useState([]);
創建一個 useEffect
函數,在應用加載及刷新時會執行它。
useEffect(()?=>?{navigator.geolocation.getCurrentPosition(function(position)?{setLat(position.coords.latitude);setLong(position.coords.longitude);});console.log("Latitude?is:",?lat)console.log("Longitude?is:",?long)},?[lat,?long]);
使用 navigator.geolocation
獲取緯度和經度,并使用 setLong 和 setLat 來設置緯度和經度 state。
import?'./App.css';
import?React,?{?useEffect,?useState?}?from?"react";
export?default?function?App()?{const?[lat,?setLat]?=?useState([]);const?[long,?setLong]?=?useState([]);useEffect(()?=>?{navigator.geolocation.getCurrentPosition(function(position)?{setLat(position.coords.latitude);setLong(position.coords.longitude);});console.log("Latitude?is:",?lat)console.log("Longitude?is:",?long)},?[lat,?long]);return?(<div?className="App"></div>);
}
app.js
現在 app.js 的內容如上。可以在瀏覽器控制臺中查看緯度和經度的值:
Latitude?is:?25.5922166
Longitude?is:?85.12761069999999
緯度和經度值
如何利用緯度和經度來獲取當前位置的天氣
創建 getWeather 函數,基于我們的緯度和經度從 API 中獲取天氣數據。
這個函數中,使用 fetch 方法調用 API 獲取天氣數據。process.env.REACT_APP_API_URL 和 process.env.REACT_APP_API_KEY 分別獲取了 .env 文件中配置的 API 地址和 API key,lat 和 long 是之前獲取到的緯度和經度。
接著將數據轉換為 JSON 格式。
再接著,使用 setData 將結果存儲在 data 對象中。
await?fetch(`${process.env.REACT_APP_API_URL}/weather/?lat=${lat}&lon=${long}&units=metric&APPID=${process.env.REACT_APP_API_KEY}`).then(res?=>?res.json()).then(result?=>?{setData(result)console.log(result);});
然后把數據打印在控制臺:
現在可以看到基于緯度和經度獲取到的天氣數據。
現在,app.js 文件的內容如下:
import?'./App.css';
import?React,?{?useEffect,?useState?}?from?"react";
import?Weather?from?'./components/weather';
export?default?function?App()?{const?[lat,?setLat]?=?useState([]);const?[long,?setLong]?=?useState([]);const?[data,?setData]?=?useState([]);useEffect(()?=>?{const?fetchData?=?async?()?=>?{navigator.geolocation.getCurrentPosition(function(position)?{setLat(position.coords.latitude);setLong(position.coords.longitude);});await?fetch(`${process.env.REACT_APP_API_URL}/weather/?lat=${lat}&lon=${long}&units=metric&APPID=${process.env.REACT_APP_API_KEY}`).then(res?=>?res.json()).then(result?=>?{setData(result)console.log(result);});}fetchData();},?[lat,long])return?(<div?className="App"></div>);
}
app.js
如何創建天氣組件
創建展示天氣數據的組件。
在 src 文件夾中,創建 components 文件夾,并在其中創建 weather.js 文件。
在 app.js 中使用天氣組件:
import?'./App.css';
import?React,?{?useEffect,?useState?}?from?"react";
import?Weather?from?'./components/weather';
export?default?function?App()?{const?[lat,?setLat]?=?useState([]);const?[long,?setLong]?=?useState([]);const?[data,?setData]?=?useState([]);useEffect(()?=>?{const?fetchData?=?async?()?=>?{navigator.geolocation.getCurrentPosition(function(position)?{setLat(position.coords.latitude);setLong(position.coords.longitude);});await?fetch(`${process.env.REACT_APP_API_URL}/weather/?lat=${lat}&lon=${long}&units=metric&APPID=${process.env.REACT_APP_API_KEY}`).then(res?=>?res.json()).then(result?=>?{setData(result)console.log(result);});}fetchData();},?[lat,long])return?(<div?className="App">{(typeof?data.main?!=?'undefined')???(<Weather?weatherData={data}/>):?(<div></div>)}</div>);
}
在 app.js 文件中引入天氣組件(Weather)
我在 return 語句加了一個判斷,如果 data.main 的值為 undefined 則展示一個空的 div。因為 fetch 函數是異步的,所以必須加入這個檢查。所有其他函數(除了異步的 fetch 函數)執行完畢之后,就會執行這個 return 語句,如果沒有這個判斷就會報錯:
這是由于我們的應用在 API 調用完成之前渲染了 return 語句中返回的內容,而此時沒有數據可以展示,所以拋出了 undefined 錯誤。
關于 async/await 的更多信息,可以查看這篇文章。
如何創建天氣組件的主體部分
這個部分我們將會使用 Semantic UI 來設計界面。(譯注:Semantic UI 使用了嚴格模式下已經棄用的 findDOMNode() 方法,會在控制臺拋出警告,不必理會。)
創建展示天氣信息的卡片:
import?React?from?'react';
import?'./styles.css';
import?{?Card?}?from?'semantic-ui-react'const?CardExampleCard?=?({weatherData})?=>?(<Card><Card.Content><Card.Header?className="header">{weatherData.name}</Card.Header></Card.Content></Card>
)export?default?CardExampleCard;
Weather.js
我們引入了并使用了 semantic-ui-react 的 Card 組件,在卡片中還有一個 Header 用來展示當前城市。
現在問題是,怎么把 app.js 中的數據傳給 weather.js 組件?
答案很簡單,可以使用 props 從父組件向子組件傳遞數據。本例中,父組件是 app.js,子組件是 weather.js。
只要在 app.js 中使用 Weather 組件的地方加上 props 即可:
<Weather?weatherData={data}/>
我們在這里通過名為 weatherData 的 props 傳入數據,就可以在 weather.js. 中接收到。
import?React?from?'react';
import?'./styles.css';
import?{?Card?}?from?'semantic-ui-react'const?CardExampleCard?=?({weatherData})?=>?(<Card><Card.Content><Card.Header?className="header">{weatherData.name}</Card.Header></Card.Content></Card>
)export?default?CardExampleCard;
可以看到,我們基于位置獲取到了城市名。
同樣的,我們可以為天氣組件加入更多字段:
import?React?from?'react';
import?'./styles.css';
import?{?Card?}?from?'semantic-ui-react'const?CardExampleCard?=?({weatherData})?=>?(<Card><Card.Content><Card.Header?className="header">City?Name:?{weatherData.name}</Card.Header><p>Temprature:?{weatherData.main.temp}</p><p>Sunrise:?{weatherData.sys.sunrise}</p><p>Sunset:?{weatherData.sys.sunset}</p><p>Description:?{weatherData.weather[0].description}</p></Card.Content></Card>
)export?default?CardExampleCard;
我們可以通過 API 獲取溫度、日出時間、日落時間以及描述信息。
可以任意添加其他字段,比如濕度、風速、能見度等。
如何格式化數據和日期
格式化數據,使其更易讀。我們會增加一些字段。
首先,為溫度加上單位,在溫度后面加上 °C。
并把日出日落時間轉換為本地時間。
import?React?from?'react';
import?'./styles.css';
import?{?Card?}?from?'semantic-ui-react'const?CardExampleCard?=?({weatherData})?=>?(<Card><Card.Content><Card.Header?className="header">City?Name:?{weatherData.name}</Card.Header><p>Temprature:?{weatherData.main.temp}?°C</p><p>Sunrise:?{new?Date(weatherData.sys.sunrise?*?1000).toLocaleTimeString('en-IN')}</p><p>Sunset:?{new?Date(weatherData.sys.sunset?*?1000).toLocaleTimeString('en-IN')}</p><p>Description:?{weatherData.weather[0].main}</p><p>Humidity:?{weatherData.main.humidity}?%</p></Card.Content></Card>
)export?default?CardExampleCard;
使用 moment.js. 計算出當天的禮拜日期和公歷日期:
import?moment?from?'moment';<p>Day:?{moment().format('dddd')}</p>
<p>Date:?{moment().format('LL')}</p>
使用 moment.js
在頂部引入 moment 包,并分別展示當天的禮拜日期和公歷日期。這個包的好處在于,它會自動更新公歷日期和禮拜日期。
現在 weather.js 內容如下:
import?React?from?'react';
import?'./styles.css';
import?{?Card?}?from?'semantic-ui-react';
import?moment?from?'moment';const?CardExampleCard?=?({weatherData})?=>?(<Card><Card.Content><Card.Header?className="header">City?Name:?{weatherData.name}</Card.Header><p>Temprature:?{weatherData.main.temp}?°C</p><p>Sunrise:?{new?Date(weatherData.sys.sunrise?*?1000).toLocaleTimeString('en-IN')}</p><p>Sunset:?{new?Date(weatherData.sys.sunset?*?1000).toLocaleTimeString('en-IN')}</p><p>Description:?{weatherData.weather[0].main}</p><p>Humidity:?{weatherData.main.humidity}?%</p><p>Day:?{moment().format('dddd')}</p><p>Date:?{moment().format('LL')}</p></Card.Content></Card>
)export?default?CardExampleCard;
weather.js
上圖是現在的頁面效果。
加上一些樣式
現在已經得到了所有數據,我們加一些樣式來讓它更有吸引力。
首先,讓卡片變大一點、改變 border-radius、加入更酷的字體和顏色、刪除文本對齊(譯注:文中 css 代碼沒有涉及文本對齊)。
import?React?from?'react';
import?'./styles.css';
import?moment?from?'moment';const?CardExampleCard?=?({weatherData})?=>?(<div?className="main"><p?className="header">{weatherData.name}</p><div><p?className="day">Day:?{moment().format('dddd')}</p></div><div><p?className="temp">Temprature:?{weatherData.main.temp}?°C</p></div></div>
)export?default?CardExampleCard;
weather.js
@import?url('https://fonts.googleapis.com/css2?family=Recursive&display=swap');.main{width:?700px;border-radius:?15px;background-color:?#01579b;
}.header{background-color:?#424242;color:?whitesmoke;padding:?10px;font-size:?28px;border-radius:?15px;font-family:?'Recursive',?sans-serif;
}.day{padding:?15px;color:?whitesmoke;font-family:?'Recursive',?sans-serif;font-size:?24px;font-weight:?600;
}.temp{padding:?15px;color:?whitesmoke;font-family:?'Recursive',?sans-serif;font-size:?18px;
}
styles.css
現在我們的應用效果如上。
使用 flexbox 來排列數據:
<div?className="flex"><p?className="day">Day:?{moment().format('dddd')}</p>
</div><div?className="flex"><p?className="temp">Temprature:?{weatherData.main.temp}?°C</p>
</div>
給 div 元素加上“flex”類,并在 styles.css. 中加入以下樣式:
.flex{display:?flex;justify-content:?space-between;
}
現在 weather.js 內容如下:
import?React?from?'react';
import?'./styles.css';
import?moment?from?'moment';const?CardExampleCard?=?({weatherData})?=>?(<div?className="main"><p?className="header">{weatherData.name}</p><div?className="flex"><p?className="day">Day:?{moment().format('dddd')}</p><p?className="day">{moment().format('LL')}</p></div><div?className="flex"><p?className="temp">Temprature:?{weatherData.main.temp}?°C</p><p?className="temp">Humidity:?{weatherData.main.humidity}?%</p></div></div>
)export?default?CardExampleCard;
同樣的,加入剩下的字段:
import?React?from?'react';
import?'./styles.css';
import?moment?from?'moment';const?WeatherCard?=?({weatherData})?=>?(<div?className="main"><p?className="header">{weatherData.name}</p><div?className="flex"><p?className="day">{moment().format('dddd')},?<span>{moment().format('LL')}</span></p><p?className="description">{weatherData.weather[0].main}</p></div><div?className="flex"><p?className="temp">Temprature:?{weatherData.main.temp}?°C</p><p?className="temp">Humidity:?{weatherData.main.humidity}?%</p></div><div?className="flex"><p?className="sunrise-sunset">Sunrise:?{new?Date(weatherData.sys.sunrise?*?1000).toLocaleTimeString('en-IN')}</p><p?className="sunrise-sunset">Sunset:?{new?Date(weatherData.sys.sunset?*?1000).toLocaleTimeString('en-IN')}</p></div></div>
)export?default?WeatherCard;
weather.js
@import?url('https://fonts.googleapis.com/css2?family=Recursive&display=swap');.main{width:?700px;border-radius:?20px;background-color:?#01579b;
}.top{height:?60px;background-color:?#424242;color:?whitesmoke;padding:?10px;border-radius:?20px?20px?0?0;font-family:?'Recursive',?sans-serif;display:?flex;justify-content:?space-between;
}.header{background-color:?#424242;color:?whitesmoke;margin:?10px?0px?0px?10px;font-size:?25px;border-radius:?20px?20px?0?0;font-family:?'Recursive',?sans-serif;
}.day{padding:?15px;color:?whitesmoke;font-family:?'Recursive',?sans-serif;font-size:?24px;font-weight:?600;
}.temp{padding:?15px;color:?whitesmoke;font-family:?'Recursive',?sans-serif;font-size:?18px;
}.flex{display:?flex;justify-content:?space-between;
}.sunrise-sunset{padding:?15px;color:?whitesmoke;font-family:?'Recursive',?sans-serif;font-size:?16px;
}.description{padding:?15px;color:?whitesmoke;font-family:?'Recursive',?sans-serif;font-size:?24px;font-weight:?600;
}
styles.css
現在我們的應用效果如下:
如何加入刷新按鈕
在頁面頂部增加一個刷新按鈕:
import?React?from?'react';
import?'./styles.css';
import?moment?from?'moment';
import?{?Button?}?from?'semantic-ui-react';const?refresh?=?()?=>?{window.location.reload();
}const?WeatherCard?=?({weatherData})?=>?(<div?className="main"><div?className="top"><p?className="header">{weatherData.name}</p><Button?className="button"?inverted?color='blue'?circular?icon='refresh'?onClick={refresh}?/></div><div?className="flex"><p?className="day">{moment().format('dddd')},?<span>{moment().format('LL')}</span></p><p?className="description">{weatherData.weather[0].main}</p></div><div?className="flex"><p?className="temp">Temprature:?{weatherData.main.temp}?°C</p><p?className="temp">Humidity:?{weatherData.main.humidity}?%</p></div><div?className="flex"><p?className="sunrise-sunset">Sunrise:?{new?Date(weatherData.sys.sunrise?*?1000).toLocaleTimeString('en-IN')}</p><p?className="sunrise-sunset">Sunset:?{new?Date(weatherData.sys.sunset?*?1000).toLocaleTimeString('en-IN')}</p></div></div>
)export?default?WeatherCard;
weather.js
.button{width:?35px;height:?35px;
}
styles.css
可以看到,頁面上多了一個刷新按鈕,點擊它就會觸發 refresh 函數、刷新頁面。
如何加入頁面加載動畫
加入加載動畫,增強應用的用戶體驗。
引入 Semantic UI 的 Loader 組件,并在數據尚未加載完成的情況下顯示:
import?{?Dimmer,?Loader?}?from?'semantic-ui-react';<div?className="App">{(typeof?data.main?!=?'undefined')???(<Weather?weatherData={data}/>):?(<div><Dimmer?active><Loader>Loading..</Loader></Dimmer></div>)}</div>
app.js
回顧一下
我們創建了一個基于地理位置展示當前天氣信息的 React 應用。
一起回顧一下我們所做的東西。
我們學習了 State 和 Props
State 和 Props 是 React 提供的非常強大的特性,可以用來管理數據、控制不同組件之間的數據流。
在我們的應用中,使用 State 管理應用程序的狀態,比如城市名稱、溫度、日期、濕度等。這些數據因人而異,取決于用戶所處的位置。
另一方面,使用 Props 在不同組件之間傳遞數據。我們在 app.js 中獲取天氣數據,又在 weather.js 中讀取這些數據。記住,使用 props 只能從父組件向子組件傳遞數據。
我們使用了 React Hooks
如果使用過類組件(class component),那么你肯定了解生命周期方法(life-cycle methods)。如果沒用過的話,可以把它們理解為一些在頁面渲染或重新渲染是執行的方法。但是我們不能在函數組件(functional component)中使用生命周期方法,因為它們是專門為類組件設計的。
所以,使用 React Hooks 來替代。在我們的應用中用到了兩個 hook,一個是用來管理應用 state 的 useState,另一個是用來在頁面渲染或加載時執行一些任務的 useEffect。
我們嘗試了 Semantic UI
Semantic UI 是為 React 設計的庫,它提供了許多出色的組件。
朋友們,以上就是本文的全部內容了。你可以試著為這個應用添加更多特性,比如未來 5 天預報、圖標說明等。
想要進一步了解的話,可以查看項目代碼。
不斷嘗試,快樂學習!
原文鏈接:https://www.freecodecamp.org/news/learn-react-by-building-a-weather-app/
作者:Nishant Kumar
譯者:Humilitas
最近組建了一個江西人的前端交流群,如果你也是江西人可以加我微信 ruochuan12 拉你進群。
·················?若川出品?·················
今日話題
今天群里有小伙伴以為我是全職運營公眾號了,我說那不得餓死,我的本職工作和大家一樣是公司寫代碼呀,業余時間運營的公眾號,目前公眾號有些收入,有了收入也會給大家送送書和一些福利。收入來源主要來自平時接的優質廣告推文,如果你剛好感興趣可以報名領取學習等,這是對我最大的支持,不感興趣劃走不看即可。歡迎分享、收藏、點贊、在看我的公眾號文章~
一個愿景是幫助5年內前端人走向前列的公眾號
可加我個人微信 ruochuan12,長期交流學習
推薦閱讀
我在阿里招前端,我該怎么幫你?(現在還能加我進模擬面試群)
若川知乎問答:2年前端經驗,做的項目沒什么技術含量,怎么辦?
點擊上方卡片關注我、加個星標,或者查看源碼等系列文章。
學習源碼整體架構系列、年度總結、JS基礎系列