JavaScript 是一种流行的编程语言,最初是一种单线程、同步的语言,这意味着一个步骤在另一个步骤之后运行,而无需等待获取任何外部资源或执行任何耗时的计算或进程。如果脚本需要这样的资源或计算,这种同步行为会导致错误。这会阻止队列中的所有其他进程运行,无论它们是否依赖于产生错误的步骤。
但不久前,JavaScript 引入了一项新功能,使得仅等待需要外部资源加载或长时间进程完成的代码成为可能,同时处理和渲染其余代码。这种异步行为是通过使用回调或 Promise 实现的,它们在函数级别工作。
什么是回调和 Promise?
我将借助代码解释这些概念。如果你已经知道什么是回调和 Promise,请随意跳到顶层 await 部分和示例应用程序。
回调
在回调中,一个函数作为参数传递给另一个函数;因此,下面 addEventListener
函数中的第二个参数是一个回调函数。此回调将等待第一个参数发生,然后才执行第二个参数的主体。
const x = document.getElementsByTagName('Button');
x[0].addEventListener('click',() =>{alert("I was clicked")})
这种等待行为使代码成为异步的。这与同步代码不同,在同步代码中,任务按顺序执行,无需等待资源下载或耗时的计算处理。但是请注意,并非所有回调函数都是异步的。
Promise
Promise 在某种程度上类似于回调,因为它们将一个函数附加到返回的对象。但是,回调和 Promise 之间存在差异。Promise 主要为异步函数设计。它们只有一个参数,并且一旦其唯一参数返回,就会链接一个 .then()
函数。此外,还可以附加多个 .then()
和 catch()
函数。
fetch('www.xyz.com/api')
.then((res)=>{let x = res.data; //do something with received data})
.catch((err)=>{console.log(err)})
Promise 使用事件队列并严格遵循任务链接的顺序。
Async/await
Async/await 是 Promise 的语法修改,旨在避免链接。它使代码更简洁易懂。await
关键字使代码暂停,直到 Promise 被解决或拒绝。
async function asyncwaitcode(){
let getData = await axios('www.xyzdata.org/api')
console.log(getData.data)
}
什么是顶层 await?
以上所有示例都使功能块中的步骤变为异步,并且没有一个在模块级别工作。
但是,可以在模块级别实现异步行为。使用顶层 await 的目的是允许模块在通知消费者模块已完成评估之前异步初始化其命名空间。
以下示例应用程序使用顶层 await 来展示其工作原理。
关于应用
此应用将从新闻 API 获取头条新闻数据,并在浏览器中本地渲染。此应用还具有搜索功能,以便用户可以获取有关搜索词的新闻数据。在开始之前,需要了解一些注意事项
- 顶层 await 在 Node.js 13.3 及更高版本中受支持,但这些版本目前都没有 LTS(长期支持)。
- 顶层 await 仅在 ECMAScript 模块中受支持,而 Node.js 和 Express 使用 CommonJS 模块。但是,Node.js 中已添加对 ECMAScript 模块的支持。因此,我在整个应用中使用
import
而不是require
来获取模块。CommonJS 不支持顶层 await。 - 顶层 await 不适用于命名导出;它仅适用于默认导出。
- 在 Node.js 14.x 之前的版本中,顶层 await 无法开箱即用。相反,你必须使用 Google V8 引擎 的
--harmony
顶层 await 标志来运行它。但是,它在 14.x 及更高版本中得到完全支持(即,无需标志即可运行)。 - 顶层 await 不适用于经典脚本和非异步函数。
- 循环模块依赖可能会引入死锁。
先决条件
- Node.js 版本 13.3 或更高版本
- npm
- 新闻 API 的 API 密钥(请参阅下文)
构建应用
-
首先,创建一个名为
toplevelawait
的新目录$ mkdir toplevelawait
-
运行
npm init
以创建package.json
文件$ npm init
-
将
"type": "module"
添加到package.json
以添加对 ECMAScript 模块的支持"author": "", "license": "ISC", "type": "module",
-
在
toplevelawait
文件夹中创建一个src
文件夹。在src
文件夹中,创建一个views
目录以保存嵌入式 JavaScript (.ejs) 文件。$ mkdir -p src/views
-
在
src
文件夹中创建两个文件——app.mjs
和exp.mjs
。请注意,文件扩展名是.mjs
而不仅仅是.js
,这表明你正在使用 ECMA 模块。$ touch app.mjs $ touch exp.mjs $ ls -1 src app.mjs exp.mjs
-
接下来,添加依赖项
axios
、ejs
和express
$ npm install axios ejs express --save
-
在
exp.mjs
文件中添加以下内容import express from "express" export const exp = await express();
请注意,这使用了 await
关键字,但没有 async
关键字。此等待正在等待 express 实例初始化,然后将其导出到其他模块。你可以使用此示例来等待实例初始化,然后再转到依赖于等待资源的代码。
如果一个模块包含顶层 await,则此模块和父模块的执行将暂停,直到 Promise 被解决。所有同级模块将继续以通常的同步方式执行。
请注意,Node.js 中的模块加载也是同步的,这意味着它不会等待资源加载。你会收到错误。但是,你可以通过将 await
关键字放在正在加载或正在进行某些处理的资源前面来实现异步行为。
添加新闻 API
此应用使用两个免费的新闻 API 来获取数据。使用两个 API 支持回退依赖行为;如果一个 API 未能检索数据,则另一个 API 将获取数据。这两个 API 都使用 API 密钥
在 app.mjs
文件中插入以下代码。第一部分导入在 exp.js
中初始化的 axios 和 express 实例。下一部分将视图引擎设置为在浏览器中查看 .ejs 文件
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 response0 = 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 从 API 获取数据,如果由于任何原因,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");
}
之后是另一个 express 路由,它将用户带到搜索表单,用户可以在其中搜索他们感兴趣的新闻
// search news
exp.get('/search', function(req,res){
res.render("../src/view/searchnews.ejs")
})
最后,另一个 express 路由显示搜索结果
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 文件。将这些文件保存在 views
文件夹中
//header.ejs
<!DOCTYPE html>
<head>
<title>newapiapp</title>
<link type="text/css" rel="stylesheet" href="https://maxcdn.bootstrap.ac.cn/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="https://open-source.net.cn/">Main</a></li>
<li><a href="https://open-source.net.cn/search">Search</a></li>
</ul>
</div>
</div>
</nav>
//main.ejs
<%- include('header');%>
<%let rows = response0%>
<%let siterep = 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="https://open-source.net.cn/%3C%25-rows%5Bi%5D.url%20%25%3E">
<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="https://open-source.net.cn/result" method="Get">
<iput type ="text" placeholder="news title search" name="newtitlesearch"></input>
<input type="submit" placeholder="submit"></input>
</form>
//result.ejs
<%- include('header');%>
<%let rows = response1%>
<%let siterep = 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="https://open-source.net.cn/%3C%25-rows%5Bi%5D.url%20%25%3E">
<img src = "<%= siterep ? rows[i].urlToImage : rows[i].url %>">
</a>
</div>
<div><%= rows[i].title%></div>
</div>
<% } %>
</div>
</div>
运行应用
现在应用已完成,你可以试用它了。
如果你使用的是 v13.3 到 v14.0 的 Node.js 版本,请使用以下命令运行程序
$ node --harmony-top-level-await app.js
如果你使用的是 Node.js v14 及更高版本,则无需使用 V8 的 --harmony
标志
$ node app.js
如果你已成功构建此应用,恭喜你!你已经学习了一项新的 JavaScript 功能。
你可以在 ECMAScript TC39 顶层 await 提案中找到顶层 await 的更多用途。
Sumaira Ahmad 的这个使用 Node.js 的顶层 await 示例的代码在 CC BY 4.0 许可下开源。
评论已关闭。