در این مقاله می خواهیم یک برنامه ساده هواشناسی را به کمک React.js بسازیم. در این برنامه با وارد کردن شهر زندگی خود، داده های هواشناسی آن را دریافت می کنید.
این مقاله برای افراد مبتدی در نظر گرفته نشده است اما تا حد ممکن مباحث ساده نیز توضیح داده شده اند و حتی اگر مبتدی باشید می توانید از مطالب این جلسه استفاده کنید. در عین حال برای مطالعه این مقاله باید با مباحث ساده CSS و جاوا اسکریپت آشنا باشید.
همچنین طرح ما برای صفحات کوچک مانند گوشی های تلفن همراه است، بنابراین در هنگام توسعه و مشاهده طرح حتما به developer tools مرورگر خود رفته و در حالت مشاهده تلفن های همراه قرار بگیرید یا اینکه پنجره مرورگر را کوچک کنید.
قدم اول برای ساخت این برنامه استفاده از پکیج Create React App است. پکیج Create React App از قبل تمام تنظیمات پیکربندی و Webpack مربوط به یک پروژه React را برایتان آماده کرده است بنابراین نیازی به ایجاد تنظیمات دستی نخواهیم داشت. برای دانلود این پکیج باید ابتدا یک نام را برای پروژه خود انتخاب کنید. من نام roxo-weather را انتخاب می کنم. در مرحله بعدی باید این پکیج را دانلود کنیم:
npx create-react-app roxo-weather
شما می توانید به جای roxo-weather هر نام دیگری را قرار بدهید اما در نظر داشته باشید که اجازه استفاده از حروف بزرگ انگلیسی را ندارید. با اجرای این دستور سوالی به شکل زیر از شما پرسیده می شود:
Need to install the following packages: create-react-app Ok to proceed? (y)
یعنی برای استفاده از پکیج Create React App باید ابتدا آن را نصب کنیم. طبیعتا باید حرف y را نوشته و enter را بزنید تا این پکیج برایتان نصب شود. فرآیند نصب و دانلود محتویات مورد نیاز آن ممکن است چندین دقیقه طول بکشد بنابراین صبور باشید. پس از اتمام عملیات چنین پیامی را در ترمینال مشاهده می کنید:
Success! Created roxo-weather at /media/amir/Development/Roxo Academy/Independent/Weather App React/roxo-weather Inside that directory, you can run several commands: npm start Starts the development server. npm run build Bundles the app into static files for production. npm test Starts the test runner. npm run eject Removes this tool and copies build dependencies, configuration files and scripts into the app directory. If you do this, you can’t go back! We suggest that you begin by typing: cd roxo-weather npm start Happy hacking!
پس از اتمام فرآیند نصب یک پوشه به نام پروژه شما برایتان ساخته می شود (در مثال من نام پروژه roxo-weather خواهد بود). شما می توانید با cd کردن در ترمینال یا به صورت عادی وارد این پوشه شوید. در هر صورت باید ترمینال یا CMD را در این پوشه باز کنید و دستور npm start را در آن اجرا کنید. با انجام این کار پروژه create-react-app در مرورگر شما باز خواهد شد. اگر این اتفاق نیفتاد، کافی است که URL نمایش داده شده در ترمینال خود را در مرورگرتان کپی کنید (این آدرس معمولا localhost:3000 است اما باز هم ترمینال خود را چک کنید). نتیجه برای من بدین شکل است:
Compiled successfully! You can now view roxo-weather in the browser. Local: http://localhost:3000 On Your Network: http://192.168.47.11:3000 Note that the development build is not optimized. To create a production build, use npm run build.
اگر قبلا با برنامه های React کار کرده باشید می دانید که create-react-app به صورت پیش فرض چند فایل ساده و یک لوگوی react را دارد تا به صورت نمونه اولیه در مرورگر نمایش دهد. من در ابتدا به پوشه src می روم و فایل های زیر را حذف می کنم چرا که در این پروژه به آن ها نیازی نداریم:
طبیعتا با حذف این فایل ها، پروژه شما در مرورگر خطای failed to compile را نمایش می دهد که طبیعی است. برای رفع این مشکل از پوشه src فایل App.js را باز کرده و دستورات import مربوط به فایل های SVG و app.css را به طور کامل حذف کنید. همچنین هیچ نیازی به کدهای درون <div> نداریم بنابراین تمام قسمت <header> را نیز حذف می کنیم. با این حساب در تابع App فقط یک تگ <div> برایتان باقی می ماند که درون آن خالی است. برای تست یک رشته ساده را در آن بنویسید:
function App() { return ( <div className="App"> Hello World </div> ); }
با ذخیره این فایل باید این رشته ساده (Hello World) را در مرورگر خود مشاهده کنید.
طبیعتا ما تجهیزات هواشناسی نداریم و حتی اگر داشتیم نیز فقط می توانیم هوای شهر خودمان را مشخص کنیم! در چنین مواقعی از API های آزاد و رایگان استفاده می شود. یکی از این API های معروف متعلق به وب سایت OpenWeatherMap می باشد. برای استفاده از API این وب سایت باید ابتدا به صفحه ثبت نام رفته و یک حساب برای خودتان بسازید. فرآیند ثبت نام بسیار ساده بوده و نیازی به دانش خاصی ندارد. شما می توانید با خرید حساب های پولی به قابلیت های بیشتری دسترسی داشته باشید اما من نیازی به آن ندارم و با حساب عادی ادامه می دهم. با حساب های عادی می توانید در هر دقیقه ۶۰ درخواست را به سرور های OpenWeatherMap ارسال کنید و در مجموع تعداد درخواست های هر ماه شما نباید بیشتر از یک میلیون درخواست باشد. این مقدار برای استفاده پروژه های شخصی عالی و بلکه بیشتر از حد نیاز است.
پس از ثبت نام در وب سایت وارد حسابتان خواهید شد. اگر به بالای صفحه و سمت راست آن نگاه کنید، نام کاربری خود را مشاهده می کنید که به معنی sign in شدن شما است. روی آن کلیک کرده و از بین گزینه های موجود My API Keys را انتخاب کنید. در این بخش یک کلید برای دسترسی به API به شما داده خواهد شد. کلید API معمولا به شکل زیر است:
97c23av7467296te866459bd17f291u9
شما برای ارسال درخواست به سرور های OpenWeatherMap به این کلید نیاز خواهید داشت.
من برای ذخیره کردن این کلید، در همان فایل App.js یک شیء ساده به نام api ساخته و این خصوصیات را در آن قرار می دهم:
const api = { key: "97c23av7467296te866459bd17f291u9", base: "https://api.openweathermap.org/data/2.5/" }
base همان base url ما است، یعنی URL پایه برای تمامی درخواست های ارسالی به سمت این API خواهد بود.
حالا که کلید API خود را دریافت کرده ایم باید در پوشه roxo-weather یک پوشه دیگر به نام assets بسازید و سپس دو تصویر زیر را دانلود کرده و در آن قرار بدهید:
ما از این دو تصویر برای ساخت پس زمینه برنامه خودمان استفاده خواهیم کرد تا بر اساس درجه گرما یا سرما یکی از این دو تصویر را در پس زمینه نمایش بدهیم.
من در قدم اول شروع به نوشتن کدهای HTML و ساختار صفحه می کنیم. طبیعتا این کار درون متد return انجام می شود:
return ( <div className="app"> <main> <div className="search-box"></div> </main> </div> );
همانطور که مشاهده می کنید من یک تگ <main> را تعریف کرده ام چرا که محتوای اصلی برنامه ما درون باید درون این تگ باشد. این بحث جزئی از مباحث معنایی و استاندارد های توصیه شده در HTML5 است اما هیچ الزامی به انجام آن نیست و می توانید به جای آن از یک div استفاده کرده و یا ساختار را به طور کامل تغییر بدهید. در مرحله بعدی یک div را داریم که کلاس search-box را دارد. ما می خواهیم input های خودمان را درون این div تعریف کنیم:
import React from "react"; function App() { return ( <div className="app"> <main> <div className="search-box"> <input type="text" className="search-bar" placeholder="Search..." /> </div> </main> </div> ); } export default App;
با ذخیره این فایل (App.js) مرورگر شما refresh شده و این input را در آن مشاهده می کنید (قابلیت Hot reload در webpack).
طبیعتا در حالت عادی این input ظاهر مناسبی نداشته و نیاز به استایل دهی دارد بنابراین به فایل index.css رفته و محتویات درون آن را پاک می کنیم. این استایل ها مربوط به همان نمونه اولیه create-react-app بوده و ما نیازی به آن نداریم. اولین استایل هایی که به این فایل اضافه می کنیم باید صفر کردن margin و padding باشد تا مقادیر margin و padding مخفی در مرورگر های مختلف باعث خراب شدن طرح ما نشوند:
* { margin: 0; padding: 0; box-sizing: border-box; }
تنظیم box-sizing روی border-box تقریبا تبدیل به یک استاندارد شده است و من پیشنهاد می کنم شما نیز از آن استفاده کنید تا محاسبه اندازه عناصر برایتان پیچیده نشود. در مرحله بعدی باید به فکر انتخاب فونت باشیم. من فونت monsterrat را برای این پروژه انتخاب کرده ام اما طبیعتا شما می توانید هر فونت دیگری را انتخاب کرده باشید:
body { font-family: 'montseratt', sans-serif; }
حالا باید به فکر پس زمینه برنامه خودمان باشیم. من دو تصویر را برایتان قرار دادم که یکی برای آب و هوای گرم و دیگری برای آب و هوای سرد بود. در اینجا سوال پیش می آید که اگر قرار است بر اساس اطلاعات هواشناسی تصویر پس زمینه را تغییر بدهیم، کدام یک را به عنوان پس زمینه انتخاب کنیم؟ تصویر پس زمینه به صورت پویا تغییر خواهد کرد بنابراین اصلا مهم نیست که از کدام تصویر به عنوان پس زمینه استفاده کنیم. من تصویر cold-bg.jpg را به صورت پیش فرض به عنوان تصویر پس زمینه انتخاب می کنم:
.app { background-image: url('./assets/cold-bg.jpg'); background-size: cover; background-position: bottom; transition: 0.4s ease; }
از این به بعد به محض بارگذاری صفحه، تصویر هوای سرد سیر در پس زمینه برنامه ما نمایش داده می شود. background-position موقعیت تصویر پس زمینه را مشخص می کند و از آنجا که اندازه برنامه ما شبیه به اندازه صفحات گوشی های موبایل است، بهتر است این مقدار را روی bottom بگذاریم تا تصویر پس زمینه دقیقا پایین صفحه قرار گرفته و صفحه را پُر کند. شما می توانید به تغییر این مقدار، حالت های مختلف آن را مشاهده کنید.
در مرحله بعدی یک کلاس دیگر به نام warm را تعریف می کنیم که مسئول نمایش پس زمینه گرم است:
.app.warm { background-image: url('./assets/warm-bg.jpg'); }
چرا؟ همانطور که گفتم تصویر پس زمینه بر اساس درجه هوا تغییر پیدا می کند بنابراین باید از قبل کلاسی داشته باشیم که تصویر گرم را نمایش بدهد. حالا می توانیم با جاوا اسکریپت این کلاس را به صفحه خود اضافه کرده و با این کار باعث تغییر تصویر پس زمینه شویم. چطور؟ کافی است کلاس warm را به div ای که کلاس app دارد اضافه کنیم.
به نظر شما با ذخیره سازی این فایل (index.css) چه اتفاقی در مرورگر می افتد؟ هیچ اتفاقی نمی افتد! چرا؟ به دلیل اینکه <main> هیچ ارتفاعی ندارد. چرا؟ به دلیل اینکه <main> هیچ محتوایی ندارد و همانطور که می دانید اگر محتوایی درون این نوع تگ ها نباشد ارتفاعشان به صورت پیش فرض صفر خواهد بود. برای حل این مشکل می گوییم:
main { min-height: 100vh; }
حالا می توانید تصویر خود را در پس زمینه مشاهده کنید. هر طراح با تجربه ای می داند که تصاویر ما در این پروژه به صورت background image یا تصویر پس زمینه هستند بنابراین نباید در معرض توجه قرار بگیرند. به طور مثال تصویر سرد ما بسیار روشن است و اگر بخواهیم نوشته ای را روی آن بنویسیم، هیچ چیزی دیده نمی شود. برای حل این مشکل و به حاشیه راندن این تصویر می توانیم از یک افکت ساده Gradient در CSS استفاده کنیم:
main { min-height: 100vh; background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.75)); padding: 25px; }
اضافه کردن این gradient باعث تیره تر شدن تصویر می شود و به نوبه خود input ما و متون ما را واضح تر می کند. من از روی سلیقه ۲۵ پیکسل را نیز به آن داده ام تا محتوای ما به گوشه های تصویر نچسبد.
در مرحله بعدی باید input خودمان را استایل دهی کنیم. من برای این کار کدهای زیر را در نظر گرفته ام:
.search-box { width: 100%; margin: 0 0 75px; } .search-box .search-bar { display: block; width: 100%; padding: 15px; appearance: none; background: none; border: none; outline: none; background-color: rgba(255, 255, 255, 0.5); border-radius: 0px 0px 16px 16px; margin-top: -25px; box-shadow: 0px 5px rgba(0, 0, 0, 0.2); color: #313131; font-size: 20px; transition: 0.4s ease; }
کدهای این بخش بسیار ساده هستند بنابراین فکر نمی کنم نیازی به توضیحات اضافه داشته باشند. با نوشتن این کد input ما حالت شیشه ای پیدا می کند و بزرگتر می شود. البته من دوست دارم در حالت focus (زمانی که کاربر روی این input کلیک کرده و آن را فعال می کند) رنگ آن روشن تر شود بنابراین می گوییم:
.search-box .search-bar:focus { background-color: rgba(255, 255, 255, 0.75); }
با این حساب محتویات فایل index.css تا این لحظه بدین شکل خواهد بود:
* { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'montseratt', sans-serif; } .app { background-image: url('./assets/cold-bg.jpg'); background-size: cover; background-position: bottom; transition: 0.4s ease; } .app.warm { background-image: url('./assets/warm-bg.jpg'); } main { min-height: 100vh; background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.75)); padding: 25px; } .search-box { width: 100%; margin: 0 0 75px; } .search-box .search-bar { display: block; width: 100%; padding: 15px; appearance: none; background: none; border: none; outline: none; background-color: rgba(255, 255, 255, 0.5); border-radius: 0px 0px 16px 16px; margin-top: -25px; box-shadow: 0px 5px rgba(0, 0, 0, 0.2); color: #313131; font-size: 20px; transition: 0.4s ease; } .search-box .search-bar:focus { background-color: rgba(255, 255, 255, 0.75); }
حالا که input را داریم باید به فایل App.js برگردیم تا شهر کاربر و تاریخ آن روز را نیز نمایش بدهیم. من می خواهم برای نمایش تاریخ تابع جداگانه ای را بنویسم که این عملیات را انجام بدهد. دو روش برای انجام این کار وجود دارد. افراد مبتدی که آشنایی کمتری با جاوا اسکریپت دارند معمولا از روش اول استفاده می کنند که اضافه نویسی زیادی دارد:
import React from "react"; function App() { const dateBuilder = d => { let months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; let days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; let day = days[d.getDay()]; let date = d.getDate(); let month = months[d.getMonth()]; let year = d.getFullYear(); return `${day} ${date} ${month} ${year}`; }; return ( <div className="app"> <main> <div className="search-box"> <input type="text" className="search-bar" placeholder="Search..." /> </div> <div> <div className="location-box"> <div className="location">New York City, US</div> <div className="date">{dateBuilder(new Date())}</div> </div> <div className="weather-box"> <div className="temp">20°c</div> <div className="weather">Sunny</div> </div> </div> </main> </div> ); } export default App;
ابتدا به قسمت return توجه کنید. ما در این بخش یک div با کلاس date را داریم که تابعی به نام dateBuilder را اجرا می کند و نمونه جدیدی از کلاس Date را ساخته و به عنوان آرگومان به آن پاس می دهد. حالا این تابع dateBuilder کجاست؟ ما آن را قبل از return تعریف کرده ایم. آرگومان d در تعریف این تابع همان شیء date است. از طرفی ما دو آرایه داریم که ماه ها و روز های هفته را به صورت مرتب در خود دارند. در مرحله بعدی از توابع جاوا اسکریپتی زیر استفاده کرده ایم:
از آنجایی که getDay و getMonth نتیجه خود را به صورت یک عدد برمی گردانند ما از آن عدد به عنوان ایندکس استفاده کرده ایم تا نتیجه را از آرایه های خودمان بگیریم و قطعات مختلف را پس از چسباندن return کرده ایم. با انجام این کار نتیجه ای مانند رشته زیر را دریافت می کنیم:
Monday 29 March 2020
روش دوم این است که به جای نوشتن آرایه ها و چسباندن قطعات مختلف کد به یکدیگر، از متد toDateString استفاده کنیم:
let today = new Date().toDateString() console.log(today);
با اجرای این کد نتیجه Mon Mar 29 2021 را در کنسول مرورگر مشاهده می کنید. شباهت نتیجه این رو روش بسیار زیاد است اما من نتیجه روش اول را ترجیح می دهم چرا که نام روز و ماه را به صورت کامل نوشته است بنابراین آن را به همان شکل اول باقی می گذارم.
در نهایت استایل دهی نوشته های ما باقی مانده است. از آنجایی که کدهای CSS در این بخش بسیار ساده هستند تمام کدهای فایل index.css را برایتان قرار می دهم:
* { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'montseratt', sans-serif; } .app { background-image: url('./assets/cold-bg.jpg'); background-size: cover; background-position: bottom; transition: 0.4s ease; } .app.warm { background-image: url('./assets/warm-bg.jpg'); } main { min-height: 100vh; background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.75)); padding: 25px; } .search-box { width: 100%; margin: 0 0 75px; } .search-box .search-bar { display: block; width: 100%; padding: 15px; appearance: none; background: none; border: none; outline: none; background-color: rgba(255, 255, 255, 0.5); border-radius: 0px 0px 16px 16px; margin-top: -25px; box-shadow: 0px 5px rgba(0, 0, 0, 0.2); color: #313131; font-size: 20px; transition: 0.4s ease; } .search-box .search-bar:focus { background-color: rgba(255, 255, 255, 0.75); } .location-box .location { color: #FFF; font-size: 32px; font-weight: 500; text-align: center; text-shadow: 3px 3px rgba(50, 50, 70, 0.5); } .location-box .date { color: #FFF; font-size: 20px; font-weight: 300; font-style: italic; text-align: center; text-shadow: 2px 2px rgba(50, 50, 70, 0.5); } .weather-box { text-align: center; } .weather-box .temp { position: relative; display: inline-block; margin: 30px auto; background-color: rgba(255, 255, 255, 0.2); border-radius: 16px; padding: 15px 25px; color: #FFF; font-size: 102px; font-weight: 900; text-shadow: 3px 6px rgba(50, 50, 70, 0.5); text-align: center; box-shadow: 3px 6px rgba(0, 0, 0, 0.2); } .weather-box .weather { color: #FFF; font-size: 48px; font-weight: 700; text-shadow: 3px 3px rgba(50, 50, 70, 0.5); }
با ذخیره کردن این کدها، UI کامل برنامه را مشاهده می کنید.
در حال حاضر UI برنامه ما کامل شده است اما ظاهر آن فقط یک سری اعداد و رشته های ساده هستند که خودمان به صورت دستی در فایل HTML تایپ کرده ایم. با این حساب تنها قدم باقی مانده اتصال این UI به API مورد نظرمان می باشد. انجام این کار از آنچه تصور می کنید ساده تر است! طبیعتا در قدم اول نیاز به state داریم بنابراین آن را وارد فایل App.js می کنیم:
import React, { useState } from 'react';
useState به ما اجازه می دهد برای کامپوننت های خودمان state تعریف کنیم. روش استفاده از آن نیز معمولا بدین شکل است دو نام را انتخاب می کنید: یکی به عنوان متغیر حاوی state و دیگری به عنوان متدی برای تغییر آن! هر نامی را که برای متغیر اول انتخاب کنید با پیشوند set برای دومی انتخاب می کنیم. react به صورت هوشمند می داند که مقادیر مناسب را به این دو متغیر بدهد:
function App() { const [query, setQuery] = useState(''); const [weather, setWeather] = useState({}); const dateBuilder = d => { let months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; let days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; let day = days[d.getDay()]; let date = d.getDate(); let month = months[d.getMonth()]; let year = d.getFullYear(); return `${day} ${date} ${month} ${year}`; }; // بقیه کدها
همانطور که می بینید من دو متغیر را برای state خودم می خواهم:
من در کد بالا مقدار اولیه هر دو متغیر state خودمان را خالی گذاشته ام (یک رشته خالی و یک شیء خالی) چرا که فعلا به آن ها دسترسی نداریم و مقادیرشان بعدا از سمت کاربر یا OpenWeatherMap به دست ما می رسند. در مرحله بعدی باید متدی را تعریف کنیم که مقدار تایپ شده توسط کاربر را از input مورد نظر گرفته و به سمت سرور های OpenWeatherMap ارسال کند. اگر به صفحه documentation این وب سایت نگاهی بیندازید در قسمت API Call مشاهده خواهید کرد که نحوه ارسال درخواست به این API باید طبق ساختار زیر باشد:
api.openweathermap.org/data/2.5/weather?q={city name}&appid={API key}
یعنی باید چند پارامتر را به base url خودمان اضافه کنیم. در ابتدا باید endpoint خود را به طور کامل مشخص کنیم چرا که وب سایت OpenWeatherMap چندین و چند نوع سرویس مختلف دارد و هواشناسی فقط یکی از آن ها است. بر این اساس ابتدا weather را به انتهای url اضافه می کنیم تا endpoint مشخص شود. در مرحله بعدی به سراغ query string انتهای URL می رویم. شما باید q را به عنوان query string پاس داده و چند پارامتر آن را مشخص می کنیم. اول از همه نام شهر را پاس داده و سپس appid یا همان کلید API خودتان را قرار می دهید.
با این حساب ما می توانیم متد خود را بر همین اساس بنویسیم. البته من یک پارامتر دیگر به نام units را نیز به آن اضافه می کنم که واحد نتایج برگردانده شده را مشخص می کند. ما از سیستم metric استفاده می کنیم (سانتی گراد به جای فارنهایت و کیلومتر به جای مایل و غیره):
const search = evt => { if (evt.key === "Enter") { fetch(`${api.base}weather?q=${query}&units=metric&APPID=${api.key}`) .then(res => res.json()) .then(result => { setWeather(result); setQuery(''); console.log(result); }); } }
از آنجایی که می خواهیم این متد را به فیلد input خود بدهیم، به صورت خودکار یک evt یا event (رویداد) دریافت خواهیم کرد. در صورتی که کلید این رویداد برابر با Enter باشد عملیات ارسال درخواست به OpenWeatherMap را شروع می کنیم. معنی key برای رویداد چیست؟ در ادامه خواهید دید که ما از attribute ای به نام onKeyPress استفاده خواهیم کرد که در هنگام فشرده شدن کلید های مختلف اجرا خواهد شد اما ما فقط به دنبال کلید enter هستیم تا زمانی که کاربر این کلید را فشار داد، عملیات جست و جو شروع شود. در ابتدا با fetch شروع کرده ایم و درخواست خودمان را ارسال می کنیم، سپس در بلوک then پاسخ سرور را دریافت کرده و متد json را روی آن صدا می زنیم. متد json پاسخ سرور را دریافت می کند و آن را در قالب یک promise برایمان برمی گرداند بنابراین دوباره یک then دیگر را نوشته ایم که این پاسخ را برابر با weather در state برنامه قرار داده است.
همانطور که گفتم نام تابع تغییر دهنده state را با پیشوند set انتخاب می کنیم که در اینجا setWeather شده است. زمانی که به این بخش برسیم یعنی آب و هوای شهر مورد نظرمان را دریافت کرده ایم بنابراین می توانیم فیلد input را پاک کنیم تا اگر کاربر بخواهد نام شهر دیگری را بنویسد، راحت تر این کار را انجام بدهد. این کار با صدا زدن setQuery و قرار دادن آن روی یک رشته خالی انجام می شود. شاید بگویید هنوز که از query هیچ استفاده ای نکرده ایم و حرفتان هم درست است اما در ادامه این کار را می کنیم. در آخرین قدم نیز نتیجه برگردانده شده را console.log کرده ایم تا با ساختار داده های دریافتی آشنا شویم.
در مرحله بعدی به سراغ input رفته و state خود را به آن متصل می کنیم. چطور؟
return ( <div className="app"> <main> <div className="search-box"> <input type="text" className="search-bar" placeholder="Search..." onChange={e => setQuery(e.target.value)} value={query} onKeyPress={search} /> </div> <div> <div className="location-box"> <div className="location">New York City, US</div> <div className="date">{dateBuilder(new Date())}</div> </div> <div className="weather-box"> <div className="temp">20°c</div> <div className="weather">Sunny</div> </div> </div> </main> </div> );
همانطور که می دانید value در یک input همان مقدار تایپ شده درون input را مشخص می کند. من value را برابر با query گذاشته ام و از آنجایی که query در ابتدای برنامه یک رشته خالی است، هیچ متنی درون input ما نمایش داده نمی شود. به همین دلیل است که پس از دریافت نتایج آب و هوا مقدار query را دوباره روی یک رشته خالی قرار داده بودیم. در مرحله بعدی متد onChange را داریم که با هر بار تغییر محتوای input اجرا می شود. ما در این حالت نیز e یا event را دریافت می کنیم (react به صورت خودکار آن را پاس می دهد) و سپس مقدارش را به setQuery پاس می دهیم. مقدار هر event ای از طریق target.value قابل دسترسی است که مربوط به جاوا اسکریپت است و اختصاصی به react ندارد.
در نهایت onKeyPress را داریم که با هر بار فشردن کلیدی از کیبورد و تایپ کاراکتر جدید در input اجرا خواهد شد. اگر یادتان باشد ما evt.key را در یک شرط if بررسی کردیم تا این کار را فقط در حالتی انجام بدهیم که کلید فشرده شده enter باشد. من در این حالت متد search را اجرا می کنم. یعنی چه؟ یعنی هر بار که کاربر نام شهر خاصی را در input تایپ کرد، متد search اجرا شده و بررسی می کند که کلید فشرده شده enter است یا خیر؟ اگر enter بود درخواست آب و هوا می کند و در غیر این صورت اصلا وارد شرط نشده و هیچ اتفاقی نمی افتد. با این حساب تمام محتویات فایل App.js باید تا این لحظه به شکل زیر باشد:
import React, { useState } from 'react'; const api = { key: "97c23av7467296te866459bd17f291u9", base: "https://api.openweathermap.org/data/2.5/" } function App() { const [query, setQuery] = useState(''); const [weather, setWeather] = useState({}); const dateBuilder = d => { let months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; let days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; let day = days[d.getDay()]; let date = d.getDate(); let month = months[d.getMonth()]; let year = d.getFullYear(); return `${day} ${date} ${month} ${year}`; }; const search = evt => { if (evt.key === "Enter") { fetch(`${api.base}weather?q=${query}&units=metric&APPID=${api.key}`) .then(res => res.json()) .then(result => { setWeather(result); setQuery(''); console.log(result); }); } } return ( <div className="app"> <main> <div className="search-box"> <input type="text" className="search-bar" placeholder="Search..." onChange={e => setQuery(e.target.value)} value={query} onKeyPress={search} /> </div> <div> <div className="location-box"> <div className="location">New York City, US</div> <div className="date">{dateBuilder(new Date())}</div> </div> <div className="weather-box"> <div className="temp">20°c</div> <div className="weather">Sunny</div> </div> </div> </main> </div> ); } export default App;
در این مرحله فایل App.js را ذخیره کرده و به مرورگر مراجعه کنید. حالا در input نام شهر مورد نظرتان را بنویسید. من Tehran را در این input نوشته و enter می زنم. با انجام این کار نتیجه زیر را در کنسول مرورگر خودتان دریافت می کنید:
{ "coord": { "lon": 51.4215, "lat": 35.6944 }, "weather": [ { "id": 800, "main": "Clear", "description": "clear sky", "icon": "01d" } ], "base": "stations", "main": { "temp": 13.42, "feels_like": 7.45, "temp_min": 13, "temp_max": 14, "pressure": 1025, "humidity": 18 }, "visibility": 10000, "wind": { "speed": 4.12, "deg": 180 }, "clouds": { "all": 0 }, "dt": 1617087002, "sys": { "type": 1, "id": 7464, "country": "IR", "sunrise": 1617071018, "sunset": 1617116020 }, "timezone": 16200, "id": 112931, "name": "Tehran", "cod": 200 }
همانطور که می بینید انواع و اقسام داده های هواشناسی مربوط به شهر تهران برای ما log شده است. تنها کاری که باقی مانده است، استفاده از این اطلاعات و نمایش آن ها در مرورگر (برای کاربر) است. این کار بسیار آسان است! تنها باید اطلاعات دریافتی از سمت این شیء JSON را گرفته و در تابع return جاسازی کنیم. من کدهای آماده شده را برایتان قرار داده و سپس توضیح خواهم داد:
return ( <div className={(typeof weather.main != "undefined") ? ((weather.main.temp > 16) ? 'app warm' : 'app') : 'app'}> <main> <div className="search-box"> <input type="text" className="search-bar" placeholder="Search..." onChange={e => setQuery(e.target.value)} value={query} onKeyPress={search} /> </div> {(typeof weather.main != "undefined") ? ( <div> <div className="location-box"> <div className="location">{weather.name}, {weather.sys.country}</div> <div className="date">{dateBuilder(new Date())}</div> </div> <div className="weather-box"> <div className="temp"> {Math.round(weather.main.temp)}°c </div> <div className="weather">{weather.weather[0].main}</div> </div> </div> ) : ('')} </main> </div> );
در ابتدا باید کلاس app یا app warm را به صورت پویا و بر اساس درجه حرارت مشخص کنیم. من برای این کار از یک اپراتور ternary ساده استفاده کرده ام که ابتدا بررسی می کند weather.main (دمای هوا) برابر undefined نباشد. چطور می شود که weather.main تعریف نشده باشد؟ اگر کاربر نام شهری را اشتباه تایپ کند و آن شهر وجود نداشته باشد یک خطای ۴۰۴ با متن city not found دریافت می کنیم بنابراین خصوصیت weather.main وجود نخواهد داشت. اگر weather.main را داشته باشیم و مقدار آن بیشتر از ۱۶ درجه باشد کلاس app warm را می دهیم تا تصویر پس زمینه تغییر کند در غیر این صورت فقط کلاس app را پاس می دهیم.
در قسمت دوم برای نمایش اطلاعات آب و هوایی شهر، باز هم از اپراتور ternary استفاده کرده ایم و شرطمان باز هم وجود weather.main است. در صورتی که weather.main وجود داشته باشد یعنی یک نتیجه داریم بنابراین اطلاعات آب و هوا را نمایش می دهیم. این اطلاعات از همان شیء JSON ای که برایتان قرار دادم به راحتی قابل استخراج است:
اگر توجه کرده باشید من از Math.round برای نمایش درجه آب و هوا استفاده کرده ام که باعث گِرد شدن قسمت اعشاری آن می شود. در صورتی که شما می خواهید درجه هوا را با اعشار نشان دهید باید قسمت Math.round را حذف کنید.
اگر طراحی UI را از ابتدای مقاله دنبال کرده باشید احتمالا متوجه حضور یک <div> اضافی در کد زیر شده اید:
{(typeof weather.main != "undefined") ? ( <div> <div className="location-box"> <div className="location">{weather.name}, {weather.sys.country}</div> <div className="date">{dateBuilder(new Date())}</div> </div> <div className="weather-box"> <div className="temp"> {Math.round(weather.main.temp)}°c </div> <div className="weather">{weather.weather[0].main}</div> </div> </div> ) : ('')}
منظور من خارجی ترین <div> (همان div پدر که بقیه کد درون آن است) می باشد. آیا می دانید چرا چنین div اضافه ای را در این بخش قرار داده ام؟ اپراتور ternary در پروژه ما فقط زمانی کار می کند که تنها یک کد واحد درونش باشد. به عبارتی اگر می خواهید از اپراتور ternary استفاده کنیم، نمی توانیم عناصر خواهر و برادری را در کنار هم داشته باشید.
کد نویسی ما در این بخش تمام شده است و حالا نوبت به تست کردن کدها می رسد. مطمئن شوید که npm start در ترمینال در حال اجرا است و شما تمام فایل های پروژه را ذخیره کرده اید. در مرحله بعدی وارد مرورگر شوید (آدرس http://localhost:3000) و نام شهری دلخواه مانند Tehran را وارد کنید. شما می توانید با وارد کردن نام شهر های سردتر و گرم تر، تغییر تصویر پس زمینه را مشاهده کنید. چند نمونه شهر سرد:
چند نمونه شهر گرم:
امیدوارم از پروژه این جلسه لذت برده باشید. سورس کد این جلسه را می توانید از لینک زیر دانلود نمایید:
پس از دانلود و استخراج فایل فشرده باید دستور npm install را اجرا کنید تا پکیج های مورد نیاز پروژه برایتان نصب شود.
در این قسمت، به پرسشهای تخصصی شما دربارهی محتوای مقاله پاسخ داده نمیشود. سوالات خود را اینجا بپرسید.