使用頂層await簡化JS代碼
JavaScript 是一種流行的編程語言,最初被設計為單線程和同步的語言,意味著程序需要在不等待獲取外部資源或者耗時的計算的情況下一步一步的運行。如果腳本需要這樣的資源或計算,這種同步行為將導致錯誤。這會阻塞隊列中的所有其他進程運行,無論它們是否依賴于那些阻塞中的任務。
但很久以前,JavaScript 引入了一個機制讓其可以在等待外部資源或者耗時任務的時候去執行其剩余的代碼。這種異步行為是通過在函數上使用回調或 promise 來實現的。
什么是回調和promise
我將通過代碼來解釋這些概念。如果你已經知道回調和 promise 是什么,請直接跳到頂層 await 部分和示例應用程序。
回調
在回調中,一個函數作為參數傳遞給另一個函數;因此,下面 addEventListener 函數中的第二個參數是回調函數。此回調將等待第一個 click 事件發生,然后才執行第二個參數。
- const x = document.getElementsByTagName('Button');
- x[0].addEventListener('click',() =>{alert("I was clicked")})
這種等待行為使代碼異步。這與同步代碼不同,它可以接著一步一步的運行,而不需要等待資源下載或者耗時進程結束。但要注意,并非所有回調函數都是異步的。
Promises
Promises 類似于回調,它將函數附加到返回的對象上。但Promises和回調也有不同,Promises 是專門為異步方法設計的。它們只有一個參數和一個用來獲得返回結果的 then() 函數。此外,它還可以鏈式的附加多個.then() 和 catch() 函數。
- fetch('www.xyz.com/api')
- .then((res)=>{let x = res.data; //do something with received data})
- .catch((err)=>{console.log(err)})
Promises使用事件隊列并嚴格遵循異步任務被鏈接的順序來執行。
Async/await
Async/await 是對 Promises 的語法改進,以避免鏈式調用。它能使代碼更清晰,更容易理解。await 關鍵字使代碼暫停,直到 Promises 成功(resolved)或者失敗(resolved)。
- async function asyncwaitcode(){
- let getData = await axios('www.xyzdata.org/api')
- console.log(getData.data)
- }
什么是頂層 await
以上所有示例都讓功能塊中的代碼異步,沒有一個在模塊級工作。
但是,異步行為是可以在模塊級別實現的。使用頂級 await 的模塊會在異步初始化其命名空間后,再通知該模塊的消費者繼續執行它自己的代碼。
下面的示例代碼展示了如何使用頂級 await。
關于App
此應用將從新聞 API 中獲取最熱門的新聞數據,并在瀏覽器中呈現。用戶還能以相關搜索詞的進行新聞數據搜索。在開始之前,有幾點需要注意:
- 頂級await在node 13.3及更高版中被支持
- 頂級await只在ECMAScript模塊中支持,但Node.js和Express都是CmmonJS模塊。CmmonJS不支持頂級await特性。所以我在代碼中會使用import而不是require
- 在node 14.x之前,頂級await不能直接使用,需要啟用--harmony
- 循環引用模塊可能會導致死鎖
構建App
1. 創建toplevelawait目錄
- $ mkdir toplevelawait
2. npm init初始化
- $ npm init
3.package.json中增加"type": "module",以支持ECMAScript模塊
- "author": "",
- "license": "ISC",
- "type": "module",
4.在toplevelawait目錄下創建src目錄。注意使用mjs作為文件后綴名。
- $ touch app.mjs
- $ touch exp.mjs
- $ ls -1 src
- app.mjs
- exp.mjs
5. 安裝依賴axios, ejs和 express
- $ npm install axios ejs express --save
6. exp.mjs中增加下面代碼:
- import express from "express"
- export const exp = await express();
注意我們是在沒有async的情況下使用await。這樣express實例會先初始化后再導出給其他模塊。你可以用這種方式等待模塊中某個實例初始化后完成后,再執行依賴于該模塊的代碼。
如果一個模塊包含了頂層await,那么它的父模塊的執行會停止,直到promise完成。但他的兄弟模塊會繼續執行,和以前的同步模式相同。
要注意Node.js中的模塊加載也是同步的,意味著他不能異步的等待資源加載。但你可以在加載或者處理資源的語句前加上await關鍵字來實現異步等待。
增加news APIs
這個應用使用兩個免費的新聞API來獲取數據。兩個API支持回退依賴行為;如果一個API失敗,另一個API將獲取數據。這兩個API都使用API密鑰:
- News API[1]
- GNews API[2]
在app.mjs文件中插入下面的代碼。前面的目標導入axios和在exp.js被初始化的express實例。下一部分設置視圖引擎,以便在瀏覽器中展示。
- import { exp } from "./exp.mjs";
- import axios from "axios"
- exp.set("view engine","ejs");
- // dependency fall back
- let response = "";
- let site = true;
- try{
- response = await axios('https://newsapi.org/v2/top-headlines?country=us&apiKey=your-api-key');
- }
- catch{
- response = await axios("https://gnews.io/api/v3/top-news?token=your-api-key");
- site = false;
- }
- // Get top news
- exp.get('/',function(req,res){
- let responseresponse0 = response.data.articles
- res.render('main.ejs',{response0: response0, site:site})
- })
- // search news
- exp.get('/search', function(req,res){
- res.render("searchnews.ejs")
- })
- exp.get('/result', async(req, res)=>{
- let x = req.query.newtitlesearch;
- let response1 = {}
- let data = {}
- try{
- let url = 'https://newsapi.org/v2/everything?q='+x+'&apiKey=your-api-key'
- response1 = await axios(url);
- }
- catch{
- let url = 'https://gnews.io/api/v3/search?q='+x+'&token=your-api-key'
- response1 = await axios(url)
- }
- res.render('result.ejs', {response1: response1.data.articles, site: site})
- })
- exp.listen(3000)
最重要的部分是這個try catch塊,使用到了頂層await來等待axios去獲取數據。如果由于任何原因,axios無法從第一個API獲取數據,應用將使用第二個API獲取數據。一旦它從API獲得數據,express可以在主頁上呈現它。
- try{
- response = await axios('https://newsapi.org/v2/top-headlines?country=us&apiKey=your-api-key');
- }
- catch{
- response = await axios("https://gnews.io/api/v3/top-news?token=your-api-key");
- }
接下來提供了一個可以讓用戶進行搜索的路由:
- // search news
- exp.get('/search', function(req,res){
- res.render("../src/view/searchnews.ejs") })
最后,另一個路徑展示搜索結果:
- exp.get('/result', async(req,res)=>{
- let x = req.query.newtitlesearch;
- let response1 = {}
- let data = {}
- try{
- let url = 'https://newsapi.org/v2/everything?q='+x+'&apiKey=your-api-key'
- response1 = await axios(url);
- }
- catch{
- let url = 'https://gnews.io/api/v3/search?q='+x+'&token=your-api-key'
- response1 = await axios(url)
- }
- res.render('../src/view/result.ejs', {response1: response1.data.articles , site: site})
- })
寫前端界面
應用的最后一部分是為前端頁面編寫四個.ejs HTML文件。將這些文件保存在“view”文件夾中:
- //header.ejs
- <!DOCTYPE html>
- <head>
- <title>newapiapp</title>
- <link type="text/css" rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" ></head><body>
- <nav class="navbar navbar-default">
- <div class="container-fluid">
- <div class="navbar-header">
- <a class="navbar-brand" href="#">News app</a>
- </div>
- <div class="collapse navbar-collapse">
- <ul class="nav navbar-nav navbar-right">
- <li><a href="/">Main</a></li>
- <li><a href="/search">Search</a></li>
- </ul>
- </div>
- </div>
- </nav>
- //main.ejs
- <%include('header');%>
- <%let rows = response0%>
- <%let sitesiterep = site%>
- <div name "container">
- <div class="row text-center" style="display:flex; flex-wrap:wrap">
- <% for(let i = 0; i < rows.length; i++){%>
- <div class="col-md-3 col-sm-6 ">
- <div class="thumbnail" >
- <a href="<%-rows[i].url %>">
- <img src = "<%= siterep ? rows[i].urlToImage : rows[i].url %>">
- </a>
- </div>
- <div><%= rows[i].title%></div>
- </div>
- <% } %>
- </div>
- </div>
- //searchnews.ejs
- <%- include('header');%>
- <h1>Search news </h1>
- <form action="/result" method="Get">
- <input type ="text" placeholder="news title search" name="newtitlesearch"></input>
- <input type="submit" placeholder="submit"></input>
- </form>
- //result.ejs
- <%- include('header');%>
- <%let rows = response1%>
- <%let sitesiterep = site%>
- <div name "container">
- <div class="row text-center" style="display:flex; flex-wrap:wrap">
- <% for(let i = 0; i < rows.length; i++){%>
- <div class="col-md-3 col-sm-6 ">
- <div class="thumbnail" >
- <a href="<%-rows[i].url %>">
- <img src = "<%= siterep ? rows[i].urlToImage : rows[i].url %>">
- </a>
- </div>
- <div><%= rows[i].title%></div>
- </div>
- <% } %>
- </div>
- </div>
運行app
現在APP已經完成,你可以嘗試下。
如果你在使用node.js v13.3 至 v14.0,運行:
- $ node --harmony-top-level-await app.js
如果你在使用node.js v14.0以上,你不需要--harmony flag:
- $ node app.js
如果你成功構建了這個app,那么恭喜你又學會了一個新的js特性。
你可以在 ECMAScript TC39 top-level await提案[3] 中獲得更多的信息。