使用 Vanilla JavaScript 框架創建一個簡單的天氣應用

大家好,不知道大家聽說過 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 創建一個簡單的天氣查詢應用。

一、看看應用長啥樣

這是一款界面十分簡潔大氣的天氣查詢應用,大概的需求是這樣的:

  • 在輸入框裏輸入城市的英文名稱,就會很直觀的展示相關城市的天氣信息,

  • 如果輸入的城市信息已經查詢過了,則會有信息提示已經查詢過此城市信息。

  • 如果輸入的城市信息不正確或者沒找到匹配的城市,應用則會提示未查詢到相關信息。

  • 查詢過的城市信息都會以列表的形式在這裏展示。

大概就是這些簡單的需求,具體界面長啥樣,如下圖所示:

交互效果,請看下段視頻展示:

是不是很漂亮呢,那還不趕緊和我一起動手完成這個應用。

二、本示例將會用到的知識點

  • flexbox 及 grid(網格)佈局

  • 媒介查詢,完成響應式的支持

  • 使用 fetch api 完成 AJAX 請求

  • 常用的JS對DOM的操作

  • ES6一些常用的新語法

三、 項目前的準備工作

1、申請天氣查詢API首先我們需要尋找一個天氣查詢的API,方便我們集成。這樣的API市面上比較多,比如阿里雲市場就可以申請,不過好像是收費的,調用起來還需要後端配合,爲了讓大家快速上手,我推薦大家去國外 https://openweathermap.org/ 這個網站申請一個免費的API,之所以用這個,調用方便,通過URL地址傳參就能進行調用,雖然高級功能需要付費,但是做個簡單的天氣查詢應用,免費功能已經夠用。

2、下載天氣圖標這個項目中,我們需要用天氣圖標直觀的展示天氣情況,這裏我建議用SVG格式的圖標,主要原因是矢量的形式,不失真,還有一個原因就是我們能根據自己的需要很方便的改變顏色。

下圖是我在網絡上找到的圖標,喜歡的可以去這裏下載:https://pan.baidu.com/s/1XS5Ua5c5SgUPiTqK_iXw7w 密碼:041m

四、創建HTML結構

基本工作準備完後,我們就開始動手實踐吧!

我們先定義兩個<p>區域,第一個 p 區域,包含了應用名稱、一個表單和一個提示信息文本。提示信息文本默認是沒內容的,只有在特定的條件下才能顯示,比如城市名稱不正確或者重複輸入已查詢過的城市信息。

第二個 p 區域用來展示已查詢過的城市列表,默認的情況,這個區域是沒有查詢信息的,只有輸入城市信息,成功調用天氣API接口時,才能顯示相關信息。

初始化的 HTML 結構如下:

<p 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>
</p>
<p class="ajax-p">
  <div class="container">
    <ul class="cities"></ul>
  </div>
</p>

autofocus 頁面初始化時,輸入焦點默認聚焦輸入表單

你會注意到第二個 p 區域裏,沒有城市列表信息,這部分的結構,是通過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-p {
  margin: 50px 0 20px;
}
 
.ajax-p .cities {
  display: grid;
  grid-gap: 32px 20px;
  grid-template-columns: repeat(4, 1fr);
}
 
@media screen and (max-width: 1000px) { 
  .ajax-p .cities {
    grid-template-columns: repeat(3, 1fr);
  }
}
 
@media screen and (max-width: 700px) {
  .ajax-p .cities {
    grid-template-columns: repeat(2, 1fr);
  }
}
 
@media screen and (max-width: 500px) {  
  .ajax-p .cities {
    grid-template-columns: repeat(1, 1fr);
  }
}

爲了讓每個城市信息的效果更加生動,類似個卡片,我們可以使用 ::after 僞元素,利用 bottom 屬性添加一個背景陰影的效果。

在這個卡片上,當接口請求成功時,我們需要展示當前城市的名稱、所屬國家、溫度及具體的天氣,天氣通過圖標和文字結合的形式進行展示,如下所示:

.ajax-p .city {
 position: relative;
 padding: 40px 10%;
 border-radius: 20px;
 background: var(--text_light);
 color: var(--text_med);
}

.ajax-p .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-p figcaption {
 margin-top: 10px;
 text-transform: uppercase;
 letter-spacing: 0.05em;
}

.ajax-p .city-temp {
 font-size: 5rem;
 font-weight: bold;
 margin-top: 10px;
 color: var(--text_dark);
}

.ajax-p .city sup {
 font-size: 0.5em;
}

.ajax-p .city-name sup {
 padding: 0.2em 0.6em;
 border-radius: 30px;
 color: var(--text_light);
 background: var(--orange);
}

.ajax-p .city-icon {
 margin-top: 10px;
 width: 100px;
 height: 100px;
}

七、添加 JavaScript

通過以上的操作我們把應用的樣式弄完了,接下來我們開始完成核心的腳本代碼。

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);

這段代碼我們兩點需要說明下:

  • 在這裏 icon 圖標變量,我們用的是 API 接口提供的,在後面的代碼中我們需要替換成我們個性化的圖標

  • 在 .city-name 元素裏面,我們添加 data-name 自定義屬性,其值包含了 cityName,countryCode,主要方便我們處理重複請求已搜索過的城市,稍後的代碼會處理。

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-p .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請求,如果不爲空,我們將觸發檢查邏輯。

  • 接下來,我們需要判斷用不是否輸入了逗號分隔用於城市+國家的形式進行精準搜索,通過 data-name 屬性進行判斷是否有重複的城市。

  • 如果檢查到有重複的城市,代碼邏輯就不進行AJAX請求,系統將會提示用戶已經查詢過此城市信息,並重置表單輸入內容爲空。

特殊邏輯說明:

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-p .cities");
const apiKey="YOUR_OWN_KEY(你申請的APIKEY)";

form.addEventListener("submit",e=>{
   e.preventDefault();
   let inputVal=input.value;
   const  listItems=list.querySelectorAll(".ajax-p .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個小時以上,如果你喜歡我的分享,麻煩給個關注、點贊加轉發哦,你的支持,就是我分享的動力,後續會持續分享實踐案例,歡迎持續關注。

延伸閱讀

動手練一練,使用 Flexbox 創建一個響應式的表單

動手練一練,用純 CSS 製作一款側滑顯示留言面板的網頁組件

使用 CSS Checkbox Hack 技術純手工擼一個手風琴組件

專注分享當下最實用的前端技術。關注前端達人,與達人一起學習進步!

長按關注"前端達人"

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章