ایجاد بخش هواشناسی در وب سایت با استفاده از Vanilla JavaScript
- مقالات آموزشی
- بروزرسانی شده در
این روزها اضافه کردن قابلیتهای پویا و کاربردی به وبسایت، تبدیل شده به یکی از نیازهای اصلی هر توسعهدهنده. یکی از همین قابلیتهای جذاب و در عین حال مفید، نمایش آبوهوای لحظهای شهرهای مختلف به کاربراست. خبر خوب اینه که برای ساختن همچین چیزی، اصلاً لازم نیست سراغ فریمورکهای سنگین و پیچیده برید.
توی این مقاله میخوایم قدمبهقدم بهتون یاد بدیم چطوری یه برنامهی هواشناسی کامل رو فقط با جاوااسکریپت خالص (Vanilla JS) و بدون هیچ کتابخانهی اضافهای بسازید. کاربر اسم هر شهری رو که بخواد جستوجو میکنه و آبوهوای همون لحظهش رو میبینه. تهش هم یه ابزار سبک، سریع و واکنشگرا دارید که روی موبایل و دسکتاپ بهخوبی کار میکنه.
ولی یه نکتهی مهم که خیلی از آموزشهای دیگه ازش رد میشن: ما فقط به ساختن برنامه بسنده نمیکنیم. بهتون میگیم چطوری کلید API رو امن نگه دارید (که سهمیهتون رو کسی نسوزونه)، و از همه مهمتر، چطوری این برنامه رو به سایت وردپرسیتون اضافه کنید — چون بالاخره اینجا میهن وردپرسه و فقط کد نوشتن کافی نیست؛ باید بدونید کجای سایت بذاریدش که درست کار کنه.
پس اگه میخواید با سادهترین روش ممکن یه برنامهی واقعاً کاربردی بسازید و روی سایتتون راه بندازید، تا آخر این مقاله همراه ما باشید. همهی کدها رو خطبهخط توضیح میدیم، طوری که حتی اگه تازهکار باشید هم هیچ جاش گیر نکنید.
آنچه در این مقاله میخوانید
چه چیزی قراره بسازیم؟
قبل از اینکه آستینها رو بزنیم بالا و بریم سراغ کد، بیاید ببینیم تهش قراره دقیقاً چی بسازیم. اینطوری هم میدونید چه انتظاری داشته باشید، هم موقع کدنویسی راحتتر میفهمید هر تیکه به چه دردی میخوره.
برنامهای که میسازیم دو بخش اصلی داره. بالای صفحه یه فرم جستوجوئه که کاربر اسم شهرش رو توش مینویسه و دکمهی جستوجو رو میزنه. پایینش هم یه قسمته که نتیجهها بهشکل کارت نمایش داده میشن. هر کارت شامل اینهاست:
- اسم شهر و کد کشور (مثلاً «رشت، IR»)
- دمای فعلی بر حسب سلسیوس
- وضعیت آبوهوا (مثلاً «تا حدودی آفتابی») همراه با آیکون متناسب با همون وضعیت

نکتهی جالبش اینه که کاربر میتونه چند شهر رو پشتسر هم جستوجو کنه و کارتهاشون کنار هم بمونن، تا بتونه آبوهوای چندتا شهر رو همزمان مقایسه کنه. در ضمن کل برنامه واکنشگرا (Responsive) هست؛ یعنی چیدمان کارتها خودش رو با اندازهی صفحه تطبیق میده — توی دسکتاپ چند ستون کنار هم، و توی موبایل تکستونی و مرتب. و چون داریم با جاوااسکریپت خالص کار میکنیم، خروجی نهایی سبک و سریعه و هیچ بار اضافهای روی سایتتون نمیذاره.
قبل از شروع، این چیزها رو باید بلد باشید:
نگران نباشید، چیز پیچیدهای لازم نیست. فقط کافیه با این سه تا یه آشنایی پایه داشته باشید:
- HTML مقدماتی: بلد باشید تگها و فرمها چطوری کار میکنن.
- CSS مقدماتی: با مفهوم کلاسها و استایلدهی پایه آشنا باشید.
- جاوااسکریپت پایه: بدونید متغیر، تابع و رویداد (event) چی هستن.
اگه با اینها راحت نیستید هم اشکالی نداره؛ چون قراره همهچی رو خطبهخط توضیح بدیم و موقع رسیدن به هر مفهوم جدید، بهتون میگیم چی به چیه.
پیشنیازهای ساخت برنامه هواشناسی
قبل از اینکه بریم سراغ کدنویسی، باید چندتا کار مقدماتی انجام بدیم: گرفتن یه کلید API معتبر و آماده کردن آیکونهای مناسب برای نمایش وضعیت آبوهوا. نگران نباشید، هر دوش کاملاً رایگانن و فقط چند دقیقه وقت میگیرن.
دریافت کلید API از OpenWeatherMap

اولین و مهمترین قدم، پیدا کردن یه سرویسدهندهی هواشناسیه که دادههای لحظهای رو بهمون بده. پیشنهاد ما OpenWeatherMap هست؛ یکی از محبوبترین و قابلاعتمادترین APIهای هواشناسی توی دنیا که نسخهی رایگانش برای ساختن یه برنامهی معمولی کاملاً کافیه. برای گرفتن کلید، این مسیر رو برید:
اول به سایت OpenWeatherMap برید و روی Sign Up کلیک کنید. توی صفحهای که باز میشه، اطلاعات لازم (ایمیل معتبر، نام کاربری و رمز عبور) رو وارد کنید. بعد از اینکه ایمیلتون رو تأیید کردید، وارد حسابتون بشید و برید به تب API Keys. اونجا یه کلید پیشفرض براتون ساخته شده؛ کپیش کنید و یه جای امن ذخیرهش کنید.
یه نکتهی خیلی مهم که وقتتون رو نجات میده: کلیدی که تازه ساختید، بلافاصله فعال نمیشه و ممکنه تا حدود یکیدو ساعت طول بکشه تا فعال بشه. پس اگه همون اول کار با خطای 401 (یا پیام Invalid API key) روبهرو شدید، نگران نباشید و فکر نکنید کدتون اشکال داره؛ فقط یهکم صبر کنید و دوباره امتحان کنید. این یکی از رایجترین چیزاییه که تازهکارها رو گیج میکنه.
دربارهی محدودیتهای نسخهی رایگان: نسخهی رایگانِ OpenWeatherMap به شما ۶۰ درخواست در دقیقه و تا ۱,۰۰۰,۰۰۰ درخواست در ماه میده که برای اکثر سایتهای کوچیک و متوسط بیشتر از حد کافیه. اگه بعداً به دادههای دقیقتر یا تعداد درخواست بیشتری نیاز داشتید، میتونید به نسخهی پولی ارتقا بدید.
یه توضیح که جلوی سردرگمیتون رو میگیره: ممکنه موقع ثبتنام، توی سایت OpenWeatherMap محصولی به اسم One Call API هم ببینید. حواستون باشه که اون یه محصول جداست و برای استفاده ازش به ثبت کارت بانکی نیاز دارید. ما توی این آموزش از endpoint کلاسیک و رایگانِ آبوهوای فعلی (یعنی /data/2.5/weather) استفاده میکنیم که نه کارت میخواد نه دردسر، و دقیقاً همون چیزیه که برای این برنامه لازم داریم.
آمادهسازی آیکونهای آبوهوایی
چیز دیگهای که برای ساختن یه اپلیکیشن جذاب لازم دارید، یه مجموعه آیکون خوشگله. خوشبختانه خودِ OpenWeatherMap یه مجموعه آیکون رسمی داره که میتونید مستقیم ازش استفاده کنید. آدرس پایهی این آیکونها اینشکلیه:
https://openweathermap.org/img/wn/[کد آیکون]@2x.png
کد هر آیکون از خود پاسخ API میاد؛ مثلاً کد 01d یعنی «آسمان صاف در روز». استفاده از این آیکونهای رسمی چندتا مزیت داره: نیازی به دانلود و میزبانی جداگانه نیست، همیشه بهروزن و سرعت بارگذاری خوبی هم دارن. توی کد جاوااسکریپت، ما آدرس آیکون رو بهصورت پویا و بر اساس کدی که از API میگیریم میسازیم و توی صفحه نشون میدیم.
برای این آموزش، توصیهی ما همین آیکونهای رسمیه چون سادهترین و سریعترین گزینهست. ولی اگه دلتون یه طراحی منحصربهفرد خواست، میتونید از مجموعههای آیکون دیگه هم استفاده کنید.
ساختار HTML برنامه هواشناسی
حالا که پیشنیازها آمادهست، وقتشه دستبهکد بشیم. اول از همه باید اسکلت اصلی صفحه رو با HTML بسازیم. ساختار ما خیلی سرراسته و دو بخش اصلی داره:
بخش بالایی (هدر و فرم جستوجو): اینجا عنوان برنامه، یه فرم برای وارد کردن اسم شهر، و یه عنصر کوچیک برای نمایش پیامهای خطا قرار میگیره.
بخش پایینی (نمایش نتیجهها): این قسمت اولش خالیه و هر بار که کاربر یه شهر رو جستوجو کنه، یه کارت جدید بهش اضافه میشه.
نکتهی مهم اینه که فرم ما نباید مثل فرمهای معمولی رفتار کنه؛ یعنی نباید با زدن دکمهی جستوجو، کل صفحه رفرش بشه. در عوض، با جاوااسکریپت جلوی این رفتار پیشفرض رو میگیریم و درخواست رو در پسزمینه (بهصورت AJAX) میفرستیم. ولی این کارو بعداً توی بخش جاوااسکریپت انجام میدیم؛ فعلاً فقط اسکلت رو بسازیم.
کد HTML پایه اینشکلیه:
html
<section class="top-banner">
<div class="container">
<h1 class="heading">اپلیکیشن آب و هوا</h1>
<form>
<input type="text" placeholder="نام شهر را جستجو کنید..." autofocus>
<button type="submit">جستجو</button>
<span class="msg"></span>
</form>
</div>
</section>
<section class="ajax-section">
<div class="container">
<ul class="cities"></ul>
</div>
</section>
بیاید ببینیم هر تیکهش چیکار میکنه:
- بخش
top-bannerهمون هدر بالای صفحهست که فرم جستوجو رو نگه میداره. - داخل
form، یهinputاز نوع متن داریم که کاربر اسم شهر رو توش مینویسه. اون ویژگیautofocusباعث میشه بهمحض باز شدن صفحه، نشانگر ماوس خودکار بره توی این کادر و کاربر بتونه بدون کلیک اضافه، تایپ کنه. - عنصر
<span class="msg">همون جاییه که پیامهایی مثل «شهر مورد نظر یافت نشد» رو بهش نشون میدیم. الان خالیه و فقط موقع لزوم پر میشه. - توی بخش پایینی، تگ
<ul class="cities">همون ظرفیه که کارتهای شهرها بهصورت پویا و با جاوااسکریپت بهش اضافه میشن. الان خالیه چون هنوز هیچ شهری جستوجو نشده.
همین! اسکلت کارمون آمادهست. الان اگه این صفحه رو توی مرورگر باز کنید، یه فرم ساده میبینید که هنوز کاری نمیکنه. توی بخشهای بعدی اول قشنگش میکنیم (با CSS) و بعد بهش جون میدیم (با جاوااسکریپت).
استایلدهی با CSS
حالا که اسکلت آمادهست، وقتشه ظاهر برنامه رو زیبا و کاربرپسند کنیم. ما از CSS مدرن استفاده میکنیم؛ یعنی متغیرها (Custom Properties)، گرید (Grid) و طراحی واکنشگرا. هدفمون اینه که برنامه روی همهی دستگاهها، از موبایل تا دسکتاپ، تمیز نمایش داده بشه.
تعریف متغیرها و بازنشانی استایلها
اول متغیرهای اصلی رنگ رو تعریف میکنیم. این کار باعث میشه اگه بعداً خواستید رنگها رو عوض کنید، فقط کافیه یه جا تغییرشون بدید و همهجا اعمال بشه:
css
:root {
--bg_main: #0a1f44;
--text_light: #fff;
--text_med: #53627c;
--text_dark: #1e2432;
--red: #ff1e42;
--darkred: #c3112d;
--orange: #ff8c00;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Tahoma, Arial, sans-serif;
background: var(--bg_main);
color: var(--text_light);
}
اون تیکهی اول (با علامت *) همهی حاشیهها و فاصلههای پیشفرض مرورگر رو صفر میکنه و مدل جعبه رو روی border-box میذاره، تا برنامه توی همهی مرورگرها یکسان دیده بشه. برای فونت هم Tahoma رو گذاشتیم که فارسی رو تمیز نشون میده.
استایل بخش بالایی و فرم
css
.top-banner {
padding: 40px 0;
text-align: center;
}
.heading {
font-size: 2rem;
margin-bottom: 20px;
}
.top-banner form {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 10px;
}
.top-banner input {
padding: 12px 16px;
border: none;
border-radius: 8px;
font-size: 1rem;
min-width: 250px;
}
.top-banner button {
padding: 12px 24px;
border: none;
border-radius: 8px;
background: var(--red);
color: var(--text_light);
font-size: 1rem;
cursor: pointer;
transition: background 0.2s;
}
.top-banner button:hover {
background: var(--darkred);
}
.top-banner .msg {
width: 100%;
margin-top: 10px;
color: var(--orange);
}
اینجا فرم رو با flex وسطچین کردیم و یهکم بهش فاصله و گردی دادیم. دکمه هم با رنگ قرمز برند و یه افکت hover ساده، جذابتر شده.
چیدمان کارتها با گرید (Grid)
حالا میرسیم به قسمت جالبش. برای چیدمان کارتهای شهرها از CSS Grid استفاده میکنیم که هم قدرتمنده هم انعطافپذیر. با یه ترفند ساده، تعداد ستونها رو میذاریم خودش بر اساس عرض صفحه تنظیم بشه:
css
.cities {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
list-style: none;
padding: 20px;
}
.city {
background: var(--text_light);
color: var(--text_dark);
border-radius: 12px;
padding: 20px;
position: relative;
overflow: hidden;
}
.city-name {
display: flex;
align-items: baseline;
gap: 6px;
}
.city-temp {
font-size: 2.5rem;
font-weight: bold;
color: var(--red);
}
.city figure {
display: flex;
align-items: center;
gap: 10px;
margin-top: 10px;
}
.city-icon {
width: 50px;
height: 50px;
}
اون خط جادویی اینجاست:
css
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
این یه خط به مرورگر میگه: «هر کارت حداقل ۲۵۰ پیکسل عرض داشته باشه، و هر چندتا که توی عرض صفحه جا شد رو کنار هم بچین.» نتیجهش اینه که توی مانیتور عریض چند ستون کنار هم میبینید، توی تبلت کمتر، و توی موبایل خودش میره روی تکستون — همهش بدون اینکه حتی یه Media Query بنویسید. همین یه خط، کل کار واکنشگرایی رو برامون انجام میده.
پشتیبانی از راستچین (RTL) برای سایتهای فارسی
چون داریم برای کاربر فارسیزبان کار میکنیم، بهتره کل برنامه راستچین باشه تا متنها و چیدمان درست و طبیعی به نظر برسن. خوشبختانه این کار فقط یه خط میخواد. کافیه به تگ اصلی برنامه (مثلاً body یا کانتینر اصلی) این رو اضافه کنید:
css
body {
direction: rtl;
}
با همین یه خط، فرم، متنها و کارتها همگی از راست به چپ مرتب میشن. فقط حواستون باشه عددها و کدهای کشور (مثل IR) و دما بهصورت لاتین باقی میمونن که این کاملاً طبیعی و درسته.
کدنویسی جاوااسکریپت و دریافت داده از API
حالا میرسیم به جذابترین قسمت، یعنی جاوااسکریپت. اینجا همونجاییه که برنامهمون از یه صفحهی ساکن، تبدیل میشه به یه ابزار واقعی و زنده.
قدم اول: انتخاب المانها و گوش دادن به فرم
اولین کار، انتخاب المانهای HTML موردنیاز و اضافه کردن یه شنوندهی رویداد (event listener) به فرمه. وقتی کاربر دکمهی جستوجو رو میزنه، ما جلوی رفتار پیشفرض فرم (همون رفرش شدن صفحه) رو میگیریم و مقدار ورودی رو میخونیم:
javascript
const form = document.querySelector(".top-banner form");
const input = document.querySelector(".top-banner input");
const msg = document.querySelector(".top-banner .msg");
const list = document.querySelector(".ajax-section .cities");
const apiKey = "YOUR_API_KEY_HERE";
form.addEventListener("submit", e => {
e.preventDefault();
const inputVal = input.value.trim();
// اگه کاربر چیزی ننوشته بود
if (!inputVal) {
msg.textContent = "لطفاً نام شهر را وارد کنید";
return;
}
// ادامهی کار...
});
اون e.preventDefault() همون خطیه که جلوی رفرش صفحه رو میگیره. input.value.trim() هم فاصلههای اضافهی اول و آخر متن رو پاک میکنه، که اگه کاربر اشتباهی یه space زد، به مشکل نخوریم.
قدم دوم: جلوگیری از جستوجوی تکراری
قبل از اینکه درخواست بفرستیم، خوبه چک کنیم که آیا این شهر از قبل توی لیست هست یا نه. اگه باشه، دیگه نیازی به درخواست دوباره نیست و فقط به کاربر اطلاع میدیم. این کار هم از کارتهای تکراری جلوگیری میکنه، هم بیخودی سهمیهی API رو نمیسوزونه:
javascript
const listItems = list.querySelectorAll(".city");
const listItemsArray = Array.from(listItems);
if (listItemsArray.length > 0) {
const filteredArray = listItemsArray.filter(el => {
let content = "";
content = el
.querySelector(".city-name span")
.textContent.toLowerCase();
return content == inputVal.toLowerCase();
});
if (filteredArray.length > 0) {
msg.textContent = "این شهر قبلاً اضافه شده است";
input.value = "";
return;
}
}
قدم سوم: فرستادن درخواست به API
حالا نوبت ارسال درخواسته. ما از Fetch API استفاده میکنیم که یه روش مدرن و مبتنی بر Promise برای درخواستهای AJAXه. آدرس URL رو با اسم شهر، کلید API و واحد سلسیوس (metric) میسازیم:
javascript
const url = `https://api.openweathermap.org/data/2.5/weather?q=${inputVal}&appid=${apiKey}&units=metric`;
fetch(url)
.then(response => response.json())
.then(data => {
// اینجا دادهها رو پردازش میکنیم
})
.catch(() => {
msg.textContent = "خطا در ارتباط با سرور. اتصال اینترنت را بررسی کنید";
});
اینجا یه نکتهی فنی مهم هست که خیلی از آموزشها اشتباه میکنن: شاید فکر کنید وقتی کاربر یه شهر نامعتبر جستوجو میکنه، خطا توی .catch() گرفته میشه. ولی اینطور نیست! وقتی شهر پیدا نشه، OpenWeatherMap یه پاسخِ موفق برمیگردونه که داخلش کد 404 نوشته شده. یعنی .catch() فقط خطاهای واقعی شبکه (مثل قطعی اینترنت) رو میگیره، نه «شهر پیدا نشد» رو. پس باید خودِ داده رو بررسی کنیم. توی قدم بعدی دقیقاً همین رو درست میکنیم.
قدم چهارم: مدیریت درست خطاها و نمایش نتیجه
حالا داخل اون .then(data => {...}) اینطوری عمل میکنیم: اول چک میکنیم کد پاسخ چیه. اگه 404 بود یعنی شهر پیدا نشده و پیام مناسب میدیم؛ در غیر این صورت دادهها رو استخراج میکنیم و کارت رو میسازیم:
javascript
.then(data => {
// اگه شهر پیدا نشد
if (data.cod === "404" || data.cod === 404) {
msg.textContent = "لطفاً یک شهر معتبر جستجو کنید";
return;
}
// استخراج دادههای موردنیاز
const { main, name, sys, weather } = data;
const icon = `https://openweathermap.org/img/wn/${weather[0].icon}@2x.png`;
// ساخت کارت جدید
const li = document.createElement("li");
li.classList.add("city");
const markup = `
<h2 class="city-name" data-name="${name},${sys.country}">
<span>${name}</span>
<sup>${sys.country}</sup>
</h2>
<span class="city-temp">${Math.round(main.temp)}<sup>°C</sup></span>
<figure>
<img class="city-icon" src="${icon}" alt="${weather[0].description}">
<figcaption>${weather[0].description}</figcaption>
</figure>
`;
li.innerHTML = markup;
list.appendChild(li);
// پاک کردن ورودی و پیام خطا
msg.textContent = "";
input.value = "";
})
بیاید ببینیم اینجا چی شد:
- اول با
data.codچک کردیم شهر پیدا شده یا نه. اون باگ معروف رو همینجا درست کردیم. - بعد با یه ترفند به اسم Destructuring، چهارتا چیزی که لازم داشتیم (
main,name,sys,weather) رو یهجا از داده بیرون کشیدیم. - آدرس آیکون رو بهصورت پویا و بر اساس کدی که از API اومده ساختیم.
- یه المان
<li>جدید درست کردیم، محتواش رو با Template Literal چیدیم، و آخرش باappendChildاضافهش کردیم به لیست. Math.roundرو هم گذاشتیم که دما رو رُند کنه و مثلاً بهجای24.56بنویسه25.
همین! حالا اگه کلیدتون فعال شده باشه و یه شهر معتبر جستوجو کنید، اولین کارت آبوهوا روی صفحه ظاهر میشه.
ذخیرهی شهرها با localStorage
تا اینجا برنامهمون کار میکنه، ولی یه ایراد داره: هر بار که کاربر صفحه رو رفرش کنه یا ببنده و دوباره باز کنه، همهی شهرهایی که جستوجو کرده بود پاک میشن و باید از اول شروع کنه. بیاید این رو درست کنیم.
راهحل، استفاده از localStorage هست؛ یه فضای ذخیرهسازی کوچیک توی مرورگر کاربر که دادهها رو حتی بعد از بستن صفحه نگه میداره. ایده سادهست: هر بار که یه شهر اضافه شد، اطلاعاتش رو توی localStorage ذخیره میکنیم، و هر بار که صفحه باز شد، شهرهای ذخیرهشده رو دوباره میچینیم.
قدم اول: ساخت یه آرایه برای نگهداری شهرها
اول یه آرایه میسازیم که لیست شهرها رو توش نگه داریم. اگه از قبل چیزی توی localStorage ذخیره شده باشه، همون رو میخونیم؛ وگرنه با یه آرایهی خالی شروع میکنیم:
javascript
let savedCities = JSON.parse(localStorage.getItem("cities")) || [];
اون JSON.parse لازمه چون localStorage فقط میتونه متن (رشته) ذخیره کنه، پس موقع خوندن باید دوباره تبدیلش کنیم به آرایه. اون || [] هم میگه «اگه چیزی ذخیره نشده بود، یه آرایهی خالی بده».
قدم دوم: ذخیره کردن شهر جدید
حالا تابعی مینویسیم که یه شهر رو به آرایه اضافه کنه و کل آرایه رو دوباره توی localStorage ذخیره کنه:
javascript
function saveCity(cityData) {
savedCities.push(cityData);
localStorage.setItem("cities", JSON.stringify(savedCities));
}
اینجا برعکسِ قبل، با JSON.stringify آرایه رو تبدیل به متن میکنیم تا بشه ذخیرهش کرد.
حالا کافیه توی همون بخشی که کارت رو میساختیم (داخل .then)، بعد از اینکه دادهها رو استخراج کردیم، این تابع رو هم صدا بزنیم و اطلاعات لازم رو بهش بدیم:
javascript
saveCity({
name: name,
country: sys.country,
temp: Math.round(main.temp),
icon: weather[0].icon,
description: weather[0].description
});
قدم سوم: نمایش شهرهای ذخیرهشده موقع باز شدن صفحه
حالا باید کاری کنیم که وقتی کاربر صفحه رو باز میکنه، شهرهای قبلیش خودکار نمایش داده بشن. برای این کار، یه تابع مینویسیم که آرایه رو بخونه و برای هر شهر یه کارت بسازه. چون این کدِ ساختِ کارت رو دوتا جا استفاده میکنیم (هم برای جستوجوی جدید، هم برای شهرهای ذخیرهشده)، بهتره ببریمش توی یه تابع جدا که کدمون تمیزتر بمونه:
javascript
function renderCity(city) {
const li = document.createElement("li");
li.classList.add("city");
const iconUrl = `https://openweathermap.org/img/wn/${city.icon}@2x.png`;
li.innerHTML = `
<h2 class="city-name" data-name="${city.name},${city.country}">
<span>${city.name}</span>
<sup>${city.country}</sup>
</h2>
<span class="city-temp">${city.temp}<sup>°C</sup></span>
<figure>
<img class="city-icon" src="${iconUrl}" alt="${city.description}">
<figcaption>${city.description}</figcaption>
</figure>
`;
list.appendChild(li);
}
// نمایش شهرهای ذخیرهشده موقع بارگذاری صفحه
savedCities.forEach(city => renderCity(city));
اون خط آخر (forEach) همونیه که جادو رو انجام میده: بهمحض باز شدن صفحه، روی همهی شهرهای ذخیرهشده میچرخه و برای هرکدوم یه کارت میسازه.
یه نکته: بعد از این تغییرات، بهتره اون کدِ ساختِ کارت توی بخش جستوجو رو هم با همین تابع renderCity جایگزین کنید تا کدتون تکراری نباشه. اینطوری هم تمیزتره، هم اگه بعداً خواستید ظاهر کارت رو عوض کنید، فقط یه جا تغییرش میدید.
با این کار، حالا برنامهی شما حافظهدار شده و شهرهای محبوب کاربر رو به خاطر میسپره.
امنیت: چرا نباید کلید API را در مرورگر بگذارید؟
تا اینجا برنامهمون کامله و کار میکنه. ولی یه مشکل مهم هست که خیلی از آموزشها کلاً ازش حرف نمیزنن، و اگه بهش توجه نکنید ممکنه بعداً برایتان دردسر بشه. بیاید صادق باشیم: توی کدی که تا الان نوشتیم، ما کلید API رو مستقیم گذاشتیم داخل جاوااسکریپتِ سمت کاربر (همون const apiKey = "...").
مشکل کجاست؟ هر چیزی که توی جاوااسکریپتِ سمت مرورگر باشه، برای همه قابل دیدنه. کافیه یه نفر روی صفحهتون کلیک راست کنه، بره توی بخش Sources مرورگر یا تب Network، و کلید شما رو راحت پیدا کنه. برای همینه که کلید API باید سمت سرور ذخیره بشه، نه داخل کدی که به مرورگر فرستاده میشه؛ چون هر کسی که سورس رو بخونه میتونه استخراجش کنه و با اون کلید، سهمیهی رایگان شما رو بسوزونه یا حتی براتون هزینه بتراشه. WordPress
پس راهحل چیه؟
ایدهی اصلی سادهست: بهجای اینکه مرورگرِ کاربر مستقیم با OpenWeatherMap حرف بزنه، یه واسطه (پراکسی) سمت سرور خودتون میذارید. یعنی:
مرورگرِ کاربر ← سرورِ شما ← OpenWeatherMap
اینطوری کلید فقط روی سرور شما میمونه و هیچوقت به مرورگر کاربر نمیره. کاربر فقط با سرور شما حرف میزنه و اصلاً خبر نداره کلید چیه.
راهحل ساده برای سایتهای وردپرسی
خبر خوب برای ما اینه که توی وردپرس، ساختن این واسطه خیلی راحته و لازم نیست سرور جدا راه بندازید. کافیه یه REST API Endpoint سفارشی بسازید که خودش درخواست رو به OpenWeatherMap بفرسته و نتیجه رو برگردونه. این کد رو میتونید توی فایل functions.php قالب فرزند یا یه افزونهی Code Snippets بذارید:
php
add_action('rest_api_init', function () {
register_rest_route('myweather/v1', '/city/(?P<name>[a-zA-Z\s]+)', array(
'methods' => 'GET',
'callback' => 'get_weather_data',
'permission_callback' => '__return_true',
));
});
function get_weather_data($request) {
$city = sanitize_text_field($request['name']);
$api_key = 'YOUR_API_KEY_HERE'; // کلید فقط اینجا، روی سرور، میمونه
$url = "https://api.openweathermap.org/data/2.5/weather?q={$city}&appid={$api_key}&units=metric";
$response = wp_remote_get($url);
if (is_wp_error($response)) {
return new WP_Error('weather_error', 'خطا در دریافت اطلاعات', array('status' => 500));
}
$body = wp_remote_retrieve_body($response);
return json_decode($body);
}
حالا توی جاوااسکریپتِ سمت کاربر، بهجای اینکه مستقیم به OpenWeatherMap درخواست بدید، به همین آدرسِ سایتِ خودتون درخواست میدید:
javascript
const url = `/wp-json/myweather/v1/city/${inputVal}`;
دیگه هیچ خبری از کلید API توی کد سمت کاربر نیست. کاربر فقط با سایت خودتون حرف میزنه و کلید، امن و دور از چشم، روی سرور میمونه.
برای کِی این کار لازمه؟ اگه فقط دارید برای تمرین و یادگیری روی سیستم خودتون کار میکنید، گذاشتن کلید توی جاوااسکریپت اشکالی نداره. ولی بهمحض اینکه خواستید این برنامه رو روی یه سایت واقعی و عمومی منتشر کنید، حتماً سراغ این روش پراکسی برید. این یه عادت حرفهایه که جلوی خیلی دردسرها رو میگیره.
بهینهسازی: کش کردن درخواستها برای نزدن به محدودیت
یادتونه گفتیم نسخهی رایگانِ OpenWeatherMap محدودیت ۶۰ درخواست در دقیقه داره؟ توی حالت عادی و برای یه سایت کوچیک، احتمالاً هیچوقت به این سقف نمیخورید. ولی اگه سایتتون پربازدید بشه یا چند کاربر همزمان یه شهر رو جستوجو کنن، ممکنه به خطای 429 (یعنی «درخواست بیش از حد») بربخورید. راهحل این مشکل، کش کردن (Caching) هست.
ایده سادهست: آبوهوا چیزی نیست که هر ثانیه عوض بشه. پس لازم نیست هر بار که کاربر یه شهر رو جستوجو میکنه، یه درخواست تازه به API بفرستیم. در عوض، نتیجه رو برای چند دقیقه نگه میداریم و اگه همون شهر دوباره جستوجو شد، نتیجهی ذخیرهشده رو نشون میدیم. چون دادهی هواشناسی کند تغییر میکنه، کش کردن مؤثرترین بهینهسازیه؛ کافیه وضعیت فعلی رو چند دقیقه نگه دارید تا راحت زیر سقف محدودیت بمونید.
اگه داری از همون روش پراکسیِ وردپرسی که بالا گفتیم استفاده میکنی، بهترین جا برای کش کردن، همون سمت سروره. وردپرس یه ابزار آماده به اسم Transients API داره که دقیقاً برای همین کار ساخته شده. کافیه تابع سمت سرور رو اینطوری تغییر بدی:
php
function get_weather_data($request) {
$city = sanitize_text_field($request['name']);
$cache_key = 'weather_' . md5(strtolower($city));
// اول چک کن آیا نتیجهی این شهر از قبل کش شده
$cached = get_transient($cache_key);
if ($cached !== false) {
return $cached; // نتیجهی ذخیرهشده رو برگردون، بدون درخواست جدید
}
$api_key = 'YOUR_API_KEY_HERE';
$url = "https://api.openweathermap.org/data/2.5/weather?q={$city}&appid={$api_key}&units=metric";
$response = wp_remote_get($url);
if (is_wp_error($response)) {
return new WP_Error('weather_error', 'خطا در دریافت اطلاعات', array('status' => 500));
}
$data = json_decode(wp_remote_retrieve_body($response));
// نتیجه رو برای ۱۰ دقیقه کش کن
set_transient($cache_key, $data, 10 * MINUTE_IN_SECONDS);
return $data;
}
حالا ببینیم چی شد:
- اول با
get_transientچک میکنیم آیا نتیجهی این شهر از قبل ذخیره شده. اگه آره، همون رو برمیگردونیم و اصلاً سراغ API نمیریم. - اگه نه، درخواست رو میفرستیم و نتیجه رو با
set_transientبرای ۱۰ دقیقه ذخیره میکنیم. - اون
MINUTE_IN_SECONDSیه ثابتِ آمادهی وردپرسه که خوانایی کد رو بهتر میکنه (بهجای اینکه خودتون بنویسید600).
با این تغییر کوچیک، اگه ۱۰۰ نفر توی ۱۰ دقیقه «تهران» رو جستوجو کنن، فقط یک درخواست به OpenWeatherMap میره و ۹۹ تای بقیه از کش جواب میگیرن. هم سریعتره، هم خیالتون از بابت محدودیت راحته.
چطور این برنامه را به وردپرس اضافه کنیم؟
خب، حالا رسیدیم به سؤالی که احتمالاً از همون اول توی ذهنتون بوده: «این کدها رو بالاخره کجای سایت وردپرسیم بذارم که کار کنه؟» این بخش دقیقاً همینه. سه روش رو معرفی میکنیم؛ از سادهترین تا حرفهایترین، که هرکدوم به سطح کار شما بخوره.
روش ۱: استفاده از بلوک Custom HTML (سادهترین راه)
اگه فقط میخواید این برنامه توی یک صفحه یا نوشتهی خاص نمایش داده بشه و دنبال سریعترین راه هستید، این روش برای شماست.
وارد ویرایش همون صفحه بشید، یه بلوک جدید از نوع «HTML سفارشی» (Custom HTML) اضافه کنید، و کل کد HTML برنامه رو توش بذارید. بعد میتونید کدهای CSS و جاوااسکریپت رو هم با تگهای <style> و <script> به همون بلوک اضافه کنید. ساده و سریعه، ولی برای کارهای بزرگتر مرتب نیست.
روش ۲: استفاده از افزونه Code Snippets
اگه میخواید کدها مرتبتر باشن و راحتتر مدیریتشون کنید (مخصوصاً اون کد PHP سمت سرور که برای امنیت نوشتیم)، بهترین گزینه افزونهی Code Snippets هست.
این افزونه رو نصب و فعال کنید. حالا میتونید کد PHP (همون REST API endpoint و کش) رو بهصورت یه اسنیپت جدا اضافه کنید، بدون اینکه نیاز باشه دست به فایل functions.php بزنید. مزیت بزرگش اینه که اگه کد ایراد داشته باشه، افزونه خودش جلوی خراب شدن سایت رو میگیره — برخلاف ویرایش مستقیم functions.php که یه اشتباه کوچیک میتونه کل سایت رو از کار بندازه.
روش ۳: ساخت شورتکد با wp_enqueue_script (روش حرفهای)
این روش بهترین و اصولیترین راهه، مخصوصاً اگه میخواید برنامه رو هر جای سایت با یه دستور ساده فراخوانی کنید. ایده اینه که یه شورتکد (مثل [weather_app]) بسازید که هرجا گذاشتیدش، برنامه اونجا ظاهر بشه. این کد رو توی functions.php قالب فرزند یا افزونهی Code Snippets بذارید:
php
function weather_app_shortcode() {
// بارگذاری فایلهای CSS و JS فقط وقتی شورتکد استفاده شده
wp_enqueue_style(
'weather-style',
get_stylesheet_directory_uri() . '/weather/style.css'
);
wp_enqueue_script(
'weather-script',
get_stylesheet_directory_uri() . '/weather/script.js',
array(),
'1.0',
true // بارگذاری در فوتر
);
// برگردوندن ساختار HTML برنامه
return '
<section class="top-banner">
<div class="container">
<h1 class="heading">اپلیکیشن آب و هوا</h1>
<form>
<input type="text" placeholder="نام شهر را جستجو کنید..." autofocus>
<button type="submit">جستجو</button>
<span class="msg"></span>
</form>
</div>
</section>
<section class="ajax-section">
<div class="container">
<ul class="cities"></ul>
</div>
</section>
';
}
add_shortcode('weather_app', 'weather_app_shortcode');
برای اینکه این روش کار کنه، کافیه فایلهای style.css و script.js رو توی یه پوشه به اسم weather داخل قالب فرزندتون بذارید. حالا هر جا که بنویسید [weather_app] (توی هر صفحه، نوشته یا حتی ابزارک)، برنامهی هواشناسی همونجا ظاهر میشه.
نکتهی مهمِ این روش، استفاده از wp_enqueue_style و wp_enqueue_script هست. این توابع روشِ استاندارد و درستِ وردپرس برای بارگذاری فایلهای CSS و JS هستن؛ یعنی فایلها فقط وقتی بارگذاری میشن که واقعاً لازمن، با هم تداخل پیدا نمیکنن، و سرعت سایتتون رو خراب نمیکنن.
توصیهی نهایی: اگه برنامهتون رو روی یه سایت واقعی منتشر میکنید، ترکیب روش ۳ (شورتکد) برای نمایش و روش پراکسیِ امن (که توی بخش امنیت گفتیم) برای کلید API بهترین و حرفهایترین حالت ممکنه.
کد نهایی و کامل
خب، رسیدیم به جایی که همهی تیکهها رو کنار هم بذاریم. توی این بخش، کد کاملِ هر سه فایل رو میذارم که بتونی مستقیم کپی کنی و استفاده کنی. این نسخه، سادهِ سمت کاربر (با کلید توی جاوااسکریپت) هست که برای تمرین و یادگیری عالیه؛ اگه خواستی منتشرش کنی، یادت باشه طبق بخش امنیت، کلید رو ببری سمت سرور.
فایل (HTML (index.html):
html
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>اپلیکیشن آب و هوا</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<section class="top-banner">
<div class="container">
<h1 class="heading">اپلیکیشن آب و هوا</h1>
<form>
<input type="text" placeholder="نام شهر را جستجو کنید..." autofocus>
<button type="submit">جستجو</button>
<span class="msg"></span>
</form>
</div>
</section>
<section class="ajax-section">
<div class="container">
<ul class="cities"></ul>
</div>
</section>
<script src="script.js"></script>
</body>
</html>
فایل CSS (style.css):
css
:root {
--bg_main: #0a1f44;
--text_light: #fff;
--text_med: #53627c;
--text_dark: #1e2432;
--red: #ff1e42;
--darkred: #c3112d;
--orange: #ff8c00;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Tahoma, Arial, sans-serif;
background: var(--bg_main);
color: var(--text_light);
direction: rtl;
}
.container {
max-width: 1000px;
margin: 0 auto;
padding: 0 20px;
}
.top-banner {
padding: 40px 0;
text-align: center;
}
.heading {
font-size: 2rem;
margin-bottom: 20px;
}
.top-banner form {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 10px;
}
.top-banner input {
padding: 12px 16px;
border: none;
border-radius: 8px;
font-size: 1rem;
min-width: 250px;
}
.top-banner button {
padding: 12px 24px;
border: none;
border-radius: 8px;
background: var(--red);
color: var(--text_light);
font-size: 1rem;
cursor: pointer;
transition: background 0.2s;
}
.top-banner button:hover {
background: var(--darkred);
}
.top-banner .msg {
width: 100%;
margin-top: 10px;
color: var(--orange);
}
.cities {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
list-style: none;
padding: 20px;
}
.city {
background: var(--text_light);
color: var(--text_dark);
border-radius: 12px;
padding: 20px;
position: relative;
overflow: hidden;
}
.city-name {
display: flex;
align-items: baseline;
gap: 6px;
}
.city-temp {
font-size: 2.5rem;
font-weight: bold;
color: var(--red);
}
.city figure {
display: flex;
align-items: center;
gap: 10px;
margin-top: 10px;
}
.city-icon {
width: 50px;
height: 50px;
}
فایل JavaScript (script.js):
javascript
const form = document.querySelector(".top-banner form");
const input = document.querySelector(".top-banner input");
const msg = document.querySelector(".top-banner .msg");
const list = document.querySelector(".ajax-section .cities");
const apiKey = "YOUR_API_KEY_HERE";
// خواندن شهرهای ذخیرهشده
let savedCities = JSON.parse(localStorage.getItem("cities")) || [];
// تابع ساخت و نمایش کارت
function renderCity(city) {
const li = document.createElement("li");
li.classList.add("city");
const iconUrl = `https://openweathermap.org/img/wn/${city.icon}@2x.png`;
li.innerHTML = `
<h2 class="city-name" data-name="${city.name},${city.country}">
<span>${city.name}</span>
<sup>${city.country}</sup>
</h2>
<span class="city-temp">${city.temp}<sup>°C</sup></span>
<figure>
<img class="city-icon" src="${iconUrl}" alt="${city.description}">
<figcaption>${city.description}</figcaption>
</figure>
`;
list.appendChild(li);
}
// تابع ذخیره شهر
function saveCity(cityData) {
savedCities.push(cityData);
localStorage.setItem("cities", JSON.stringify(savedCities));
}
// نمایش شهرهای ذخیرهشده موقع بارگذاری
savedCities.forEach(city => renderCity(city));
// مدیریت ارسال فرم
form.addEventListener("submit", e => {
e.preventDefault();
const inputVal = input.value.trim();
if (!inputVal) {
msg.textContent = "لطفاً نام شهر را وارد کنید";
return;
}
// جلوگیری از شهر تکراری
const exists = savedCities.some(
city => city.name.toLowerCase() === inputVal.toLowerCase()
);
if (exists) {
msg.textContent = "این شهر قبلاً اضافه شده است";
input.value = "";
return;
}
const url = `https://api.openweathermap.org/data/2.5/weather?q=${inputVal}&appid=${apiKey}&units=metric`;
fetch(url)
.then(response => response.json())
.then(data => {
// بررسی شهر نامعتبر
if (data.cod === "404" || data.cod === 404) {
msg.textContent = "لطفاً یک شهر معتبر جستجو کنید";
return;
}
const { main, name, sys, weather } = data;
const cityData = {
name: name,
country: sys.country,
temp: Math.round(main.temp),
icon: weather[0].icon,
description: weather[0].description
};
renderCity(cityData);
saveCity(cityData);
msg.textContent = "";
input.value = "";
})
.catch(() => {
msg.textContent = "خطا در ارتباط با سرور. اتصال اینترنت را بررسی کنید";
});
});
همین! این سه فایل رو کنار هم بذارید (توی یه پوشه)، کلید API خودتون رو جایگزین YOUR_API_KEY_HERE کنید، و فایل index.html رو توی مرورگر باز کنید. برنامهتون آمادهی استفادهست.
سؤالات متداول
چرا کلید API من کار نمیکند و خطای 401 میدهد؟
شایعترین دلیلش اینه که کلیدتون رو همین الان ساختید. کلیدهای جدید OpenWeatherMap بلافاصله فعال نمیشن و ممکنه تا حدود یکیدو ساعت طول بکشه. پس اگه کدتون درسته ولی خطای ۴۰۱ میگیرید، یهکم صبر کنید و دوباره امتحان کنید. دلیل دوم هم ممکنه این باشه که کلید رو اشتباه کپی کردید (مثلاً یه فاصلهی اضافه چسبیده بهش)؛ یه بار دیگه از داشبورد کپیش کنید.
آیا استفاده از OpenWeatherMap واقعاً رایگان میماند؟
بله. نسخهی رایگانش با محدودیت ۶۰ درخواست در دقیقه و یک میلیون درخواست در ماه، برای همیشه رایگانه و برای اکثر سایتهای کوچیک و متوسط بیشتر از حد کافیه. فقط حواستون باشه که محصول جداگانهی One Call API به کارت بانکی نیاز داره؛ ولی ما توی این آموزش از endpoint رایگان استفاده کردیم که اصلاً کارت نمیخواد.
یک جایگزین بدون نیاز به کلید API وجود دارد؟
بله، اگه اصلاً نمیخواید درگیر گرفتن کلید بشید، میتونید از Open-Meteo استفاده کنید. این سرویس برای پروژههای شخصی و تستی، بدون نیاز به ثبتنام و کلید کار میکنه و گزینهی خوبیه برای تازهکارها. البته ساختار پاسخش با OpenWeatherMap فرق داره، پس باید کد پردازش دادهها رو متناسبش تغییر بدید.
چطور بهجای جستوجوی دستی، از موقعیت خود کاربر استفاده کنم؟
میتونید از Geolocation API مرورگر استفاده کنید که موقعیت کاربر (با اجازهی خودش) رو میگیره. کافیه با navigator.geolocation.getCurrentPosition() طول و عرض جغرافیایی رو بگیرید و بهجای پارامتر q=نامشهر، از lat و lon توی آدرس API استفاده کنید. اینطوری بهمحض ورود کاربر، آبوهوای شهر خودش خودکار نشون داده میشه.
چرا دادهها را سمت سرور کش کنیم بهتر از سمت کاربر است؟
چون کش سمت کاربر (مثل localStorage) فقط برای همون یک کاربر کار میکنه. ولی وقتی سمت سرور کش میکنید، اگه ۱۰۰ نفر یه شهر رو جستوجو کنن، فقط یک درخواست به API میره و بقیه از کش جواب میگیرن. این هم سرعت رو بالا میبره هم خیلی مؤثرتر جلوی پر شدن سهمیهتون رو میگیره.
جمعبندی
تبریک میگم! تا اینجا یه برنامهی هواشناسیِ کامل و حرفهای رو فقط با جاوااسکریپت خالص ساختید. برنامهای که آبوهوای هر شهر دنیا رو بهصورت لحظهای نشون میده، شهرهای محبوب کاربر رو توی حافظهی مرورگر نگه میداره، و روی همهی دستگاهها از موبایل تا دسکتاپ تمیز کار میکنه.
ولی کاری که توی این آموزش کردیم، فراتر از یه برنامهی ساده بود. ما چندتا نکتهی مهم رو هم یاد گرفتیم که خیلی از آموزشها ازشون رد میشن:
- خطاها رو درست مدیریت کردیم (مخصوصاً اون باگ معروفِ «شهر یافت نشد» که توی
.catch()گرفته نمیشه). - یاد گرفتیم چرا نباید کلید API رو توی مرورگر بذاریم و چطوری با یه پراکسی امن در وردپرس ازش محافظت کنیم.
- با کش کردن درخواستها، هم سرعت رو بالا بردیم هم خیالمون از بابت محدودیت API راحت شد.
- و از همه مهمتر، یاد گرفتیم چطوری این برنامه رو با سه روش مختلف واقعاً به یه سایت وردپرسی اضافه کنیم.
نکتهی خوب اینه که کل کدی که نوشتیم، استاندارد و مبتنی بر جاوااسکریپت مدرنه و هیچ کتابخانه یا فریمورک خارجیای استفاده نکردیم. برای همین برنامهی شما سبک، سریع و قابلاعتماده.
از اینجا به بعد دستتون بازه که برنامه رو بهتر کنید: میتونید قابلیت حذف هر شهر با کلیک روی یه دکمه رو اضافه کنید، پیشبینی چندروزه رو نمایش بدید، یا با Geolocation شهر کاربر رو خودکار تشخیص بدید. استایلش رو هم میتونید با رنگهای برند خودتون شخصیسازی کنید.
امیدواریم این آموزش براتون مفید بوده باشه و حالا یه ابزار هواشناسیِ حرفهای روی سایتتون داشته باشید. اگه جایی گیر کردید یا سؤالی داشتید، توی بخش نظرات بپرسید؛ خوشحال میشیم کمکتون کنیم. موفق و پیروز باشید! 🙂
برای ارسال نظر لطفا ابتدا ثبتنام کنید یا وارد شوید.