vanilla
在今天的教程中,我们将学习如何使用Vanilla JavaScript构建简单但功能齐全的天气应用程序。 我们有很多有趣的事情要讲,所以请喝杯咖啡,让我们开始吧!
我们将要建设的
这是一个介绍性视频,演示了我们将要创建的应用程序的功能:
注意:本教程假定您熟悉AJAX(一项基本的前端技术)。 如果您才刚刚开始,请查看本系列 。
1.搭建应用程序
在开始创建我们的应用程序之前,我们需要考虑一些事项。
查找天气API
首先,我们必须找到一个提供商,让我们将其天气数据整合到我们的应用程序中。 幸运的是,这里有几个不同的提供商可以开发天气应用程序。 它们中的大多数包括免费软件包以及根据服务/功能而扩展的高级订阅。
在我们的案例中,我们将使用OpenWeatherMap ,这是最受欢迎的免费选择之一。 要利用其功能,首先,我们必须注册一个API密钥:
此服务附带不同的软件包。 从下面的图表中可以看到,该启动器(免费)每分钟允许60个呼叫,这符合我们的需求:
因此,在继续之前,请确保您已经注册了API密钥。 稍后,我们将把该密钥包含在脚本中。
请记住,测试应用程序的最佳方法是派生Codepen演示并包含您自己的密钥。 如果我们所有人都共享同一个密钥,则由于API调用限制,该应用程序可能无法运行。
查找天气图标
接下来,出于本教程的目的,我们将需要一堆天气图标。 值得注意的是,OpenWeatherMap带有自己的图标集,我们将对其进行介绍。 但是,我们将更进一步,并使用一些自定义的方法。
再一次,我们将利用Envato Elements库并下载一包矢量天气图标 :
2.定义页面标记
我们将定义两个部分。
第一部分将包括一个标题,一个搜索表单,和一个空的span元件。 在某些情况下,此元素将显示并带有适当的消息。 具体来说,如果没有所请求城市的天气数据,或者该城市的数据是已知的。
第二部分将包括城市列表。 默认情况下,它将不包含任何城市。 但是,当我们开始搜索特定城市的天气时,如果天气数据可用,则会将相应的列表项(城市)附加到无序列表中。
这是初始页面标记:
<section class="top-banner">
<div class="container">
<h1 class="heading">Simple Weather App</h1>
<form>
<input type="text" placeholder="Search for a city" autofocus>
<button type="submit">SUBMIT</button>
<span class="msg"></span>
</form>
</div>
</section>
<section class="ajax-section">
<div class="container">
<ul class="cities"></ul>
</div>
</section>
注意:在我们的Codepen演示中,搜索字段的autofocus属性不起作用。 实际上,如果您打开浏览器控制台,它将引发以下错误:
但是,如果您在本地运行此应用程序(而不是作为Codepen项目),则不会存在此问题。
这是与我们将通过JavaScript动态生成的列表项相关的标记:
<li class="city">
<h2 class="city-name" data-name="...">
<span>...</span>
<sup>...</sup>
</h2>
<span class="city-temp">...<sup>°C</sup></span>
<figure>
<img class="city-icon" src="..." alt="...">
<figcaption>...</figcaption>
</figure>
</li>
2.指定一些基本样式
准备好应用程序的标记后,我们将继续使用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;
font-weight: normal;
}
button {
cursor: pointer;
}
input {
-webkit-appearance: none;
}
button,
input {
border: none;
background: none;
outline: none;
color: inherit;
}
img {
display: block;
max-width: 100%;
height: auto;
}
ul {
list-style: none;
}
body {
font: 1rem/1.3 "Roboto", sans-serif;
background: var(--bg_main);
color: var(--text_dark);
padding: 50px;
}
4.设置主要样式
现在让我们讨论应用程序的主要样式。
第1节样式
首先,我们将在第一部分的元素中添加一些简单的样式。
在中等屏幕及以上(> 700像素)的屏幕上,布局应如下所示:
在较小的屏幕上,表单元素将分为两行:
以下是相关的样式:
/*CUSTOM VARIABLES HERE*/
.top-banner {
color: var(--text_light);
}
.heading {
font-weight: bold;
font-size: 4rem;
letter-spacing: 0.02em;
padding: 0 0 30px 0;
}
.top-banner form {
position: relative;
display: flex;
align-items: center;
}
.top-banner form input {
font-size: 2rem;
height: 40px;
padding: 5px 5px 10px;
border-bottom: 1px solid;
}
.top-banner form input::placeholder {
color: currentColor;
}
.top-banner form button {
font-size: 1rem;
font-weight: bold;
letter-spacing: 0.1em;
padding: 15px 20px;
margin-left: 15px;
border-radius: 5px;
background: var(--red);
transition: background 0.3s ease-in-out;
}
.top-banner form button:hover {
background: var(--darkred);
}
.top-banner form .msg {
position: absolute;
bottom: -40px;
left: 0;
max-width: 450px;
min-height: 40px;
}
@media screen and (max-width: 700px) {
.top-banner form {
flex-direction: column;
}
.top-banner form input,
.top-banner form button {
width: 100%;
}
.top-banner form button {
margin: 20px 0 0 0;
}
.top-banner form .msg {
position: static;
max-width: none;
min-height: 0;
margin-top: 10px;
}
}
第2节样式
我们将使用CSS Grid布置列表项。 请记住,每个列表项将代表一个城市。 它们的宽度将取决于屏幕尺寸。
在大屏幕(> 1000像素)上,我们将采用四列布局。
然后在中型屏幕(> 700px和≤1000px)上三列布局,在小屏幕(> 500px和≤700px)上两列布局,最后在超小屏幕(≤500px)上将所有元素堆叠在一起。
以下是相应的样式:
.ajax-section {
margin: 50px 0 20px;
}
.ajax-section .cities {
display: grid;
grid-gap: 32px 20px;
grid-template-columns: repeat(4, 1fr);
}
@media screen and (max-width: 1000px) {
.ajax-section .cities {
grid-template-columns: repeat(3, 1fr);
}
}
@media screen and (max-width: 700px) {
.ajax-section .cities {
grid-template-columns: repeat(2, 1fr);
}
}
@media screen and (max-width: 500px) {
.ajax-section .cities {
grid-template-columns: repeat(1, 1fr);
}
}
每列看起来像一张带有底部阴影的卡片,将通过::after伪元素添加。
在卡片内,我们将放置有关所请求城市的天气信息。 这些将来自我们的请求,除了图标。 如上所述,这些图标是从Envato Elements中获取的,它们将显示该城市的当前天气状况,并与等效的OpenWeatherMap图标匹配。
在下面,您可以看到此布局所需的一部分CSS:
/*CUSTOM VARIABLES HERE*/
.ajax-section .city {
position: relative;
padding: 40px 10%;
border-radius: 20px;
background: var(--text_light);
color: var(--text_med);
}
.ajax-section .city::after {
content: ’’;
width: 90%;
height: 50px;
position: absolute;
bottom: -12px;
left: 5%;
z-index: -1;
opacity: 0.3;
border-radius: 20px;
background: var(--text_light);
}
.ajax-section figcaption {
margin-top: 10px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.ajax-section .city-temp {
font-size: 5rem;
font-weight: bold;
margin-top: 10px;
color: var(--text_dark);
}
.ajax-section .city sup {
font-size: 0.5em;
}
.ajax-section .city-name sup {
padding: 0.2em 0.6em;
border-radius: 30px;
color: var(--text_light);
background: var(--orange);
}
.ajax-section .city-icon {
margin-top: 10px;
width: 100px;
height: 100px;
}
5.添加JavaScript
至此,我们已经准备好构建应用程序的核心功能。 我们开始做吧!
提交表格时
每次用户通过按Enter键或Submit按钮提交表单时,我们都会做两件事:
- 停止提交表单,从而防止重新加载页面。
- 抓住搜索字段中包含的值。
这是开始代码:
const form = document.querySelector(".top-banner form");
form.addEventListener("submit", e => {
e.preventDefault();
const inputVal = input.value;
});
接下来,我们将检查第二部分是否有列表项(城市)。
执行AJAX请求
我们将从列表为空的假设开始。 也就是说,它过去从未运行过任何AJAX请求。 在这种情况下,我们将执行对OpenWeatherMap API的请求,并传递以下参数:
- 城市名称(例如,雅典)或逗号分隔的城市名称以及国家/地区代码(例如,雅典,gr),将成为搜索字段的值
- API密钥。 同样,您应该使用自己的密钥来避免由于API调用限制而导致的意外错误。
- 所请求城市的温度单位。 在我们的案例中,我们将选择Celcius。
考虑到以上所有内容,通过遵循API文档 ,我们的请求URL应该如下所示:
const apiKey = "YOUR_OWN_KEY";
const inputVal = input.value;
...
const url = `https://api.openweathermap.org/data/2.5/weather?q=${inputVal}&appid=${apiKey}&units=metric`;
为了执行AJAX请求,我们有很多选择。 我们可以使用普通的旧XMLHttpRequest API ,较新的Fetch API ,甚至可以使用JavaScript库,例如jQuery和Axios 。 对于此示例,我们将使用Fetch API。
要获取所需的数据,我们必须执行以下操作:
- 将我们要访问的URL传递给
fetch()方法。 - 此方法将返回一个包含响应的Promise(一个
Response对象)。 但这不是实际的响应,而只是HTTP响应。 为了获取所需的JSON格式(这是OpenWeatherMap的默认数据格式)的响应数据,我们将使用Response对象的json()方法。 - 此方法将返回另一个Promise。 完成后,数据将可供操作。
- 如果由于某种原因请求失败,则相应的消息将出现在屏幕上。
因此,我们的AJAX请求如下所示:
...
fetch(url)
.then(response => response.json())
.then(data => {
// do stuff with the data
})
.catch(() => {
msg.textContent = "Please search for a valid city 😩";
});
提示:不用链接then() ,我们可以对AJAX请求使用更新且更易读的async/await方法。
这是响应数据的示例:
构建列表项组件
有了AJAX请求,每次我们在搜索字段中输入城市时,API将返回其天气数据(如果有)。 现在,我们的工作是仅收集所需的数据,然后创建关联的列表项,最后将其附加到无序列表。
这是负责这项工作的代码:
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>
<div class="city-temp">${Math.round(main.temp)}<sup>°C</sup>
</div>
<figure>
<img class="city-icon" src=${icon} alt=${weather[0]["main"]}>
<figcaption>${weather[0]["description"]}</figcaption>
</figure>
`;
li.innerHTML = markup;
list.appendChild(li);
这里我们要讨论两件事:
- 如果再次查看上面的响应可视化,您会注意到API返回了一个
icon代码(例如“ 50d”),其中包含目标城市的当前天气状况 。 基于此代码,我们可以构造图标URL并通过img标签将其显示在卡中。 - 在每个列表项的
.city-name元素内,我们将在data-name属性后附加值为cityName,countryCode(例如madrid,es)。 稍后,我们将使用此值来防止重复请求。
重设事物
最后,在AJAX请求之后,我们将清除.msg元素的内容,搜索字段的值,并同时关注该字段:
...
msg.textContent = "";
form.reset();
input.focus();
伙计们,干得好! 我们刚刚创建了应用程序的第一个版本。 当您放置自己的API密钥并搜索城市时,您应该会看到类似于该卡片的布局:
这是相关的Codepen演示:
添加自定义图标
现在让我们自定义应用程序的外观。 我们将用我们先前从Envato Elements下载的SVG替换默认的OpenWeatherMap PNG图标。
为此,我已经将所有新图标(通过我是PRO成员通过资产管理器)上传到Codepen并更改了它们的名称,因此它们将与原始图标的名称和天气情况相匹配,如下所示:
然后,在代码中,我们只需更改图标路径:
//BEFORE
const icon = `https://openweathermap.org/img/wn/${
weather[0]["icon"]
}@2x.png`;
//AFTER
const icon = `https://s3-us-west-2.amazonaws.com/s.cdpn.io/162656/${
weather[0]["icon"]
}.svg`;
防止重复请求
我们仍然需要解决一件事。 到目前为止,当我们执行成功的AJAX请求时,将创建一个列表项。 也就是说,列表可以包含多个引用同一城市的相同列表项,如下所示:
那是糟糕的用户体验,因此,请确保针对特定城市仅触发一个请求。
但是在此之前,还有另一件事需要考虑。 同一城市的名称可以存在于多个国家/地区。 例如,如果我们在OpenWeatherMap的搜索器中搜索“雅典”,我们将看到以下结果:
考虑到以上所有内容,我们将编写一些代码,以确保每个城市,每个国家/地区仅执行一个请求:
...
//1
const listItems = list.querySelectorAll(".ajax-section .city");
const listItemsArray = Array.from(listItems);
if (listItemsArray.length > 0) {
//2
const filteredArray = listItemsArray.filter(el => {
let content = "";
//athens,gr
if (inputVal.includes(",")) {
//athens,grrrrrr->invalid country code, so we keep only the first part of inputVal
if (inputVal.split(",")[1].length > 2) {
inputVal = inputVal.split(",")[0];
content = el.querySelector(".city-name span").textContent.toLowerCase();
} else {
content = el.querySelector(".city-name").dataset.name.toLowerCase();
}
} else {
//athens
content = el.querySelector(".city-name span").textContent.toLowerCase();
}
return content == inputVal.toLowerCase();
});
//3
if (filteredArray.length > 0) {
msg.textContent = `You already know the weather for ${
filteredArray[0].querySelector(".city-name span").textContent
} ...otherwise be more specific by providing the country code as well 😉`;
form.reset();
input.focus();
return;
}
}
让我解释一下这里发生了什么动作:
- 再次在提交处理程序期间,在发出AJAX请求之前,我们检查无序列表是否为空。 如果不为空,则意味着至少已成功执行一个AJAX请求。
- 接下来,我们检查是否有一个列表项,其城市名称或其
data-name属性的值等于搜索字段的值。 - 如果是这样,则意味着用户已经知道该城市的天气,因此无需执行其他AJAX请求。 作为以下操作,我们将向他们显示相关消息,清除搜索字段的值并为其赋予焦点。
注意#1:正如我已经注意到的,如果您搜索的城市中最多包含两个不代表任何国家代码的字母(例如athens,aa),则该API将不会返回任何内容。 另一方面,如果您搜索城市以及至少三个不代表任何国家/地区代码的字母(例如athens,aaaa),则API将忽略逗号后的部分,并返回所有名称为第一部分(例如雅典)。
注意#2:在本练习中,我们也不会涵盖一个国家包含多个同名城市的特殊情况(例如,美国的雅典)。 因此,例如,如果用户搜索“雅典,我们”,则只会在屏幕上显示一个城市,而不会显示更多。 为了涵盖理想的情况,用户应该以某种方式知道城市ID(例如,使它们可作为下拉菜单使用),然后根据该ID进行搜索,而不是根据其名称进行搜索。
伙计们,干得好! 我们刚刚构建了我们的应用程序。 让我们来看看:
结论
我们完成了! 这确实是一段漫长的旅程,但我希望您喜欢它,它有助于提高您的前端技能。
再一次,别忘了为实时应用程序测试投入自己的密钥!
提醒一下,让我们再次看一下该应用程序的工作方式:
与往常一样,非常感谢您的阅读!
您可以做很多事情来扩展此应用程序的功能。 这里有一些想法:
如果您还有其他想要作为应用扩展看到的内容,请在下面的评论中告诉我!
vanilla
788

被折叠的 条评论
为什么被折叠?



