大家好,不知道大家听说过 Vanilla JavaScript 这款框架吗?最近我在浏览国外的一些技术网站时,这个词出现的频率实在是太高了,好多框架都宣称自己是基于 Vanilla JavaScript。那到底什么是 Vanilla JavaScript 呢?
vanilla-js.com 官方网站是这样介绍的:Vanilla JS is a fast, lightweight, cross-platform framework for building incredible, powerful JavaScript applications.
Vanilla JS 是一个快速、轻量级、跨平台的JavaScript框架。我们可以用它构建强大的JavaScript应用程序。
大家是不是觉得很这个框架很强大呢,哈哈,不和大家卖关子了,Vanilla JavaScript 就是原生JavaScript。现在做前端项目,大家是不是都在用vue、react这样的框架呢,遇到一些复杂的功能和效果,就是想寻找是否有相关的插件呢,很少想到手工实现呢?大家是否想过这些问题,如果没有这些前端框架,我们是否还能顺利完成项目呢?
本篇文章,我将和大家一起使用原生 JavaScript 创建一个简单的天气查询应用。
这是一款界面十分简洁大气的天气查询应用,大概的需求是这样的:
大概就是这些简单的需求,具体界面长啥样,如下图所示:
交互效果,请看下段视频展示:
是不是很漂亮呢,那还不赶紧和我一起动手完成这个应用。
1、申请天气查询API首先我们需要寻找一个天气查询的API,方便我们集成。这样的API市面上比较多,比如阿里云市场就可以申请,不过好像是收费的,调用起来还需要后端配合,为了让大家快速上手,我推荐大家去国外 https://openweathermap.org/ 这个网站申请一个免费的API,之所以用这个,调用方便,通过URL地址传参就能进行调用,虽然高级功能需要付费,但是做个简单的天气查询应用,免费功能已经够用。
2、下载天气图标这个项目中,我们需要用天气图标直观的展示天气情况,这里我建议用SVG格式的图标,主要原因是矢量的形式,不失真,还有一个原因就是我们能根据自己的需要很方便的改变颜色。
下图是我在网络上找到的图标,喜欢的可以去这里下载:https://pan.baidu.com/s/1XS5Ua5c5SgUPiTqK_iXw7w 密码:041m
基本工作准备完后,我们就开始动手实践吧!
我们先定义两个<section>区域,第一个 section 区域,包含了应用名称、一个表单和一个提示信息文本。提示信息文本默认是没内容的,只有在特定的条件下才能显示,比如城市名称不正确或者重复输入已查询过的城市信息。
第二个 section 区域用来展示已查询过的城市列表,默认的情况,这个区域是没有查询信息的,只有输入城市信息,成功调用天气API接口时,才能显示相关信息。
初始化的 HTML 结构如下:
<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>
autofocus 页面初始化时,输入焦点默认聚焦输入表单
你会注意到第二个 section 区域里,没有城市列表信息,这部分的结构,是通过JS代码动态生成的,基本结构如下:
<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>
<sup> 用来展示上角标文本。
创建完基本的结构后,我们需要用 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;
}
基础样式完成后,我们需要为两个 Section 区域添加样式
1、Section #1 Styles
首先我们需要完善下 Section 区域一的样式,当前屏幕大于 >700px,界面如下图所示:
当前屏幕小与等于700px时,应用名称、输入框、按钮各占一行,界面如下图所示:
完后的样式代码如下所示:
.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、Section #2 Styles
这部分区域,我们将用到网格布局进行展示城市天气信息列表,当然这部分区域也是要支持响应式的。
如果当前屏幕大于1000px,我们一行将展示4个城市信息,如下图所示:
当屏幕在 (>700px and ≤1000px) 时,显示三列;当屏幕 (>500px and ≤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 伪元素,利用 bottom 属性添加一个背景阴影的效果。
在这个卡片上,当接口请求成功时,我们需要展示当前城市的名称、所属国家、温度及具体的天气,天气通过图标和文字结合的形式进行展示,如下所示:
.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;
}
通过以上的操作我们把应用的样式弄完了,接下来我们开始完成核心的脚本代码。
1、当按钮提交时
当用户点击按钮或者按回车键时,我们的程序应该这么做:
代码的基础结构如下所示:
const form = document.querySelector(".top-banner form");
form.addEventListener("submit", e => {
e.preventDefault();
const inputVal = input.value;
});
接下来我们来处理,如何展示城市列表的数据信息。
2、执行 AJAX 请求
假设第一次进入页面,城市列别还没相关信息,这种情况我们只需要发送 OpenWeatherMap 的 API 请求即可,遵循 API 文档,我们只需要传递申请的 API 的 key,城市名称即可,如下段代码所示:
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`;
基于文档说明,我们通过JS自带的 fetch() 请求方法,处理AJAX请求,具体的示例代码如下:
...
fetch(url)
.then(response => response.json())
.then(data => {
// do stuff with the data
})
.catch(() => {
msg.textContent = "Please search for a valid city ";
});
下图为我们请求过来的数据格式:
3、编写单个城市卡片组件
数据请求成功后,我们就需要处理数据,展示城市的天气信息,填充到城市列表展示区域,相关代码如下所示:
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);
这段代码我们两点需要说明下:
4、重置表单输入接口请求完后,我们需要将表单输入框置空,提示信息置空,输入焦点重新聚焦到输入框。示例代码如下:
msg.textContent = "";
form.reset();
input.focus();
5、替换成自己的个性化图标
如下图所示,以下接口自带的几种图片我们需要一一对应成我们自己个性化的图标,名称也保持一致,放到我们的图片文件夹即可:
对应代码需要做相应的修改,如下所示:
//BEFORE
const icon = `https://openweathermap.org/img/wn/${
weather[0]["icon"]
}@2x.png`;
//AFTER
const icon = `images/${
weather[0]["icon"]
}.svg`;
6、阻止相同城市请求
为了防止多次提交同一个城市的信息 ,我们需要进行去重,要不就会发生如下的效果,并不是我们期望的:
这是个糟糕的用户体验,除此之外,还需要处理一个情况,如果一个城市,比如 Athens,在希腊是雅典,在美国为雅典-克拉克县,这种情况不能认为是重复的请求,我们支持用逗号分隔输入,前面城市后面国家简写。
基于以上的去重需求,刚才前面提及到的自定义 data-name 就派上用场了,完后的代码如下所示:
...
//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;
}
}
接下来,我来解释下上述代码的一些关键点:
特殊逻辑说明:
Note #1: 如果你通过逗号的形式精确搜索时,如果国家简写不正确的化(两个字母简写,比如 athens,aa),API接口不会返回任何信息。如果你输多于三个字母的国家简写,而且没有意义(比如 athens,aaaa),API接口 则会不考虑逗号的部分,则按照城市的信息默认搜索,比如直接返回希腊的雅典。
Note #2: 如果一个城市属于多个国家,没有进行逗号精准搜索的话,API 接口也不会把所有相关国家的城市都罗列出来,只会显示一个城市而已。
7、最后贴上完整的 JS 代码
'use strict';
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_OWN_KEY(你申请的APIKEY)";
form.addEventListener("submit",e=>{
e.preventDefault();
let inputVal=input.value;
const listItems=list.querySelectorAll(".ajax-section .city");
const listItemsArray=Array.from(listItems);
if(listItems.length>0){
const filteredArray=listItemsArray.filter(el=>{
let content="";
if(inputVal.includes(",")){
if(inputVal.split(",")[1].length>2){
inputVal=inputVal.split(",")[0];
content=el.querySelector(".city-name span").textContent.toLocaleLowerCase();
}else{
content=el.querySelector(".city-name").dataset.name.toLowerCase();
}
} else{
content=el.querySelector(".city-name span").textContent.toLowerCase();
}
return content===inputVal.toLowerCase();
});
if(filteredArray.length>0){
msg.textContent=`Your 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 here
const url=`https://api.openweathermap.org/data/2.5/weather?q=${inputVal}&appid=${apiKey}&units=metric`;
fetch(url)
.then(response=>response.json())
.then(data=>{
const { main,name,sys,weather } =data;
const icon=`images/${weather[0]["icon"]}.svg`;
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]["description"]}"
<figcaption>${weather[0]["description"]}</figcaption>
</figure>
`;
li.innerHTML=markup;
list.appendChild(li);
})
.catch(()=>{
msg.textContent="please search for a valid city ";
});
msg.textContent="";
form.reset();
input.focus();
});
到这里我们的代码终于完成了,是不是很长,希望你能看下去,建议你还是亲手动手实践一遍享受下代码实践的成就感,这个应用还有许多地方改需要改进,比如ajax的等待请求提示,输入格式的验证等等,有兴趣的可以自己尝试下。本示例大家可以点击 阅读原文 进行在线体验。
写完这篇原创文章已是凌晨12点多,从实践到写文章花了将近12个小时以上,如果你喜欢我的分享,麻烦给个关注、点赞加转发哦,你的支持,就是我分享的动力,后续会持续分享实践案例,欢迎持续关注。