1、Hello Node.js v20
市面上的Node.js书籍绝大部分是Node.js 8版本以前的,而现在最新的Node.js版本是v20。从Node.js 8到Node.js v20,整整12个版本,从2017到2023,整整6年时间里,变化是极大的。
本章内容主要是帮助初学者更容易的掌握Node.js v20简单项目的如何入门进行讲解的,内容相对简单,但知识点还是比较多的。即使作为一个很多年经验的Node.js开发者,对于这里面的知识点很多也不一定都清楚的。无论哪种情况,笔者都建议大家好好学习一下这一章节。
如果想写好一个Node.js v20简单的入门项目,你可能会涉及到已下3个主要变化。
- 模块规范从CommonJS过渡到ESM,必须要掌握ESM,CommonJS已经是可选了。
- 异步编程模块,从回调函数到Promise,从Promise到过渡到Async函数,必须掌握Async函数,其他可选。
- Node.js 内置了test runner,不再需要mocha、tap、tape、ava、jest、vitest等。
下面我们会对这些变化进行一一讲解。
Node.js v20
2023年4 月 18 日,Node.js 正式发布了 v20 版本,是当前最新的可用版本。从Node.js 8之后,很长一段时间,都是围绕如何兼容Web新规范而演进,之所以要讲解v20这个版本,主要有5个原因。
- 变化较多,从Node.js 8到v20,可以说是翻天覆地的变化,无论Node.js新手,还是老手,都值得重新学习一下。
- 截止到v20版本,新特性基本趋于稳定,像ESM、Async函数等都已经非常成熟了。
- 搭配前端最新Web规范,很多Node.js知识点需要更新,比如内置fetch等。
- Deno、Bun等服务端JavaScript运行时崛起,其实Node.js v20已经有很多与时俱进的内容,比如network-import等,依然是当前主流,稳定,可靠的。
- AIGC爆火,带动Node.js全栈需求更加旺盛。结合LLM模型(OpenAI或Replicate等),Langchain.js,以及https://github.com/vercel-labs/ai SDK,组合数据库操作,可以快速落地AI业务。
Node.js是什么
按照官方介绍https://nodejs.dev/en/learn/,通过5个要点来讲解Node.js是什么,具体如下。
1、JavaScript运行时环境
Node.js 是一个开源和跨平台的 JavaScript 运行时环境。 它是几乎任何类型项目的流行工具!Node.js 具有独特的优势,因为数百万为浏览器编写 JavaScript 的前端开发者现在除了客户端代码之外,还能够编写服务器端代码,而无需学习完全不同的语言。
这里需要说明的是,JavaScript优先,但也支持C/C++/Rust,通过N-Api(之前叫Nan node addon)扩展即可。
Node.js主要场景是服务器端代码,却在前端工程领域无心插柳柳成荫,变成了大前端领域必备的组成部分。下面讲Node.js应用场景的时候会细讲。
2、基于v8,所以快
Node.js 在浏览器之外运行 V8 JavaScript 引擎(Google Chrome 的内核)。 这使得 Node.js 执行速度非常高效。
很多c代码写的算法,都不一定比v8写的js代码执行速度快。参考https://github.com/felixge/faster-than-c
3、单进程单线程,简单
Node.js 应用在单个进程中运行,无需为每个请求创建新线程。 Node.js 在其标准库中提供了一组异步 I/O ,以防止 JavaScript 代码阻塞,并且通常Node.js 中的库是使用非阻塞范例编写的,这使得阻塞行为成为异常而不是常态。
缺点:部署服务端代码的时候,一般你需要根据多少个cpu核数n来决定起n-1个实例,这时候就需要pm2这样的进程管理工具,很多人人吐槽pm2,其根本原因是Node.js自身机制问题。
4、基于libuv实现的事件循环,成败皆因此
当 Node.js 执行 I/O 操作时,如从网络读取、访问数据库或文件系统,Node.js 不会阻塞线程和浪费 CPU 周期等待,而是会在事件循环完成恢复操作。
当Node.js进程启动时,Node会创建一个类似于while(true){...}的EventLoop(浏览器也有),每执行一次循环体的过程我们称为Tick。每个Tick的过程就是查看是否有事件待处理,如果有,就取事件及其相关的回调函数。如果存在关联的回调函数,就执行他们。然后进入下一个循环,如果不再有事件要处理,就退出进程。以后学的深入的时候,需要setImmediate 和 process.nextTick,就是围绕EventLoop操作的相关API。
这使得 Node.js 可以使用单个服务器处理数千个并发连接,而不会引入管理线程并发的负担(这可能是灾难的来源)。
基于EventLoop,任务是异步的,所以要采用Error-first Callback写法,所以导致后面异步流程极其复杂,且编写代码时,需要时刻注意EventLoop里加入的代码是否为异步,不然就可能出现性能问题。
辩证的看,Node.js的好处是让你不需要关注多线程就能实现高性能,但你需要关注事件循环是否为异步。
果然,天下没有免费的午餐。
5、跟进Web标准
在 Node.js 中,可以毫无问题地使用新的 ECMAScript 标准(部分),因为你不必等待所有用户更新他们的浏览器(但要等v8更新) - 你负责通过更改 Node.js 版本来决定使用哪个 ECMAScript 版本(这其实不是特性,而是不完善), 你还可以通过运行带有标志的 Node.js 来启用特定的实验性特性(这种一般就是玩玩)。
说的有点夸张了,但跟进是事实,且一定是有滞后现象的。
Node.js应用场景
1、最重要的场景是Server场景(初心)
2、最通用的是Cli工具开发场景(通用)
3、占比较大的前端相关场景(当前最多,除了Server、API,其他基本都算)
Node.js v20运行原理和新特性
- node v20特性:std lib 在标准化,user lib 在精细化
- v8+eventloop+promise-base api(error-first)+event
- npm
- esm
- async/await + promise
- worker thead(tinypool)
- loader
- test runner
- 权限模型
- 可观测性,包括 logging/metrics/tracing,以及 APM 等
- 现代化的 HTTP:undici
- wasm
- n-api
本节主要讲解Node.js v20入门,大家了解一下即可,限于篇幅,下一门课会详细讲具体细节。
Node.js和大前端的关系
讲Node.js应用场景的时候,我们说过占比较大的前端相关场景,除了Server、API,其他基本都算是大前端场景的组成部分。
下面我们来看一下Node.js和大前端的关系。
Node.js在大前端中发挥了重要作用,主要包括以下方面:
- 服务器端开发:Node.js可以作为服务器端语言来处理HTTP请求,实现服务器端的逻辑处理和数据存储等功能。
- 前端构建工具:Node.js提供了npm包管理工具,开发者可以通过npm下载和管理各种前端框架、库和插件等。
- 前端自动化构建:Node.js可以结合gulp、grunt、webpack、vite等自动化构建工具来进行前端代码的自动化打包、压缩、合并等操作,提高开发效率。
- 实时通信:Node.js可以通过socket.io等技术实现实时通信,例如聊天室、在线游戏等,比如hmr等。
- 数据库操作:Node.js可以通过mongoose等库来进行数据库的操作,例如数据的增删改查等。
- 人工智能:Node.js可以结合TensorFlow等机器学习框架来进行人工智能的开发和应用。
- 云计算:Node.js可以结合AWS Lambda等云计算平台来进行云计算相关的开发和应用。
总之,Node.js在大前端中的应用非常广泛,可以帮助开发者快速搭建服务器、构建前端、实现自动化构建、进行实时通信、操作数据库、开发人工智能等。
Node.js安装
这部分参考了《狼书(卷1):更了不起的Node.js》里的3m安装法。
nvm(node version management)
首先介绍第一种 nvm,Github 地址 https://github.com/nvm-sh/nvm,文档写的也很详细,推荐去阅读,以下为安装步骤:
- 安装 nvm:wget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.33.2/install.sh | bash
- 查看所有 Node.js 版本:nvm ls-remote
- 查看本地 Node.js 版本:nvm ls
- 安装 Node.js:nvm install v6.9.5
- 设置系统的默认 Node.js 版本:nvm alias default v6.9.5
Example:
nvm install 8.0.0 Install a specific version number
nvm use 8.0 Use the latest available 8.0.x release
nvm run 6.10.3 app.js Run app.js using node 6.10.3
nvm exec 4.8.3 node app.js Run `node app.js` with the PATH pointing to node 4.8.3
nvm alias default 8.1.0 Set default node version on a shell
nvm alias default node Always default to the latest available node version on a shell
nvm install node Install the latest available version
nvm use node Use the latest version
nvm install --lts Install the latest LTS version
nvm use --lts Use the latest LTS version
nvm set-colors cgYmW Set text colors to cyan, green, bold yellow, magenta, and white
如果是windows平台,推荐使用nvs。
npm(node package management)
npm(Node Package Manager)是Node.js的包管理工具,用于安装、发布和管理Node.js模块。它是Node.js的默认包管理工具,随同Node.js一起安装。
最新特性包括:
- npx:npx是npm 5.2.0版本引入的新命令,用于执行临时安装的模块。它可以直接运行本地安装的模块,而无需全局安装。例如,可以使用npx来运行项目依赖的模块,而无需在全局安装它们。
- npx create:npx create是一种快速创建项目的方式,它可以通过执行命令"npx create-
"来创建一个新的项目。例如,可以使用"npx create-react-app my-app"来创建一个新的React应用程序。
与yarn和pnpm相比,npm是最流行的包管理工具之一,拥有大量的社区支持和生态系统。
- yarn是由Facebook开发的另一种包管理工具,它具有更快的安装速度和更好的缓存机制。
- pnpm是一个相对较新的包管理工具,它的主要优势是节省磁盘空间和更快的安装速度。
虽然pnpm大有后来居上的意味,但是,npm的生态系统更加完善,而且npm的新特性也在不断地更新和完善,因此npm仍然是最受欢迎的包管理工具之一。
为了解决npm、yarn、pnpm等各种包的管理问题,Node.js官方提供了Corepack工具,它是一个用于管理和分发Node.js核心模块的工具,旨在提供一个简单、可靠的方式来访问和使用Node.js核心模块。
Node.js Corepack的主要特性包括:
- 简化的安装和使用:Corepack可以通过一条简单的命令行指令来安装和更新Node.js核心模块。它提供了一个统一的接口,使得安装和使用核心模块变得更加简单和方便。
- 版本管理:Corepack允许你在不同的Node.js版本之间切换,并且可以管理和安装不同版本的核心模块。这对于开发者来说非常有用,可以轻松地切换和管理不同版本的Node.js核心模块。
- 快速的下载和更新:Corepack使用了高效的下载和缓存机制,可以快速地下载和更新核心模块。这样可以节省时间,并且可以在没有网络连接的情况下进行离线安装和更新。
- 可靠的分发:Corepack使用了Node.js团队提供的官方分发源,确保了核心模块的可靠性和安全性。它还支持自定义分发源,使得开发者可以使用自己的私有分发源。
总的来说,Node.js Corepack是一个方便、可靠的工具,可以帮助开发者更好地管理和使用Node.js核心模块。它简化了安装和更新的过程,提供了版本管理和快速下载的功能,同时保证了核心模块的可靠性和安全性。
Corepack目前还不是默认开启的功能,暂时先了解就可以了。
nrm(node registry management)
- https://www.npmjs.com/ 官方源。
- https://npmmirror.com/ 国内源,之前叫cnpm。
常识
- 国内安装会比国外快。所以能用https://npmmirror.com/就尽量用
- 内网安装会比外网快。能在内网使用cnpm搭建一个npm私有源,就尽量用。
无论哪种情况,我们都需要切换npm源。
安装
$ npm install -g nrm
查看支持的源
$ nrm ls
* npm ---------- https://registry.npmjs.org/
yarn --------- https://registry.yarnpkg.com/
tencent ------ https://mirrors.cloud.tencent.com/npm/
cnpm --------- https://r.cnpmjs.org/
taobao ------- https://registry.npmmirror.com/
npmMirror ---- https://skimdb.npmjs.com/registry/
切换源
$ nrm use cnpm //switch registry to cnpm
Registry has been set to: http://r.cnpmjs.org/
ESM (ECMAScript Modules)
当下前端主流用法是基于ESM方式进行编写,Node.js v20已经非常好的支持ESM了,推荐大家使用这种方式进行编写。
为了能够让大家理解ESM前世今生,本节我们也花了一点时间了解模块规范演进历史和必须了解的CommonJS,最后给出了现在Node.js里最常用的引用写法。
下面我们从模块规范演进历史开始进行一一讲解。
模块规范演进历史
JavaScript模块规范的发展历史可以追溯到早期的CommonJS规范,然后发展到ESM(ECMAScript Modules)规范。下面是一个简要的描述:
- CommonJS规范:在Node.js出现之前,JavaScript缺乏一种官方的模块化规范。为了解决这个问题,CommonJS规范在2009年提出,它定义了一种模块加载和导出机制,使得开发者可以将代码组织成独立的模块,并在需要时进行加载和使用。CommonJS规范主要用于服务器端的JavaScript开发,Node.js采用了这个规范。
- CommonJS规范定义了
require
和module.exports
等关键字,用于加载和导出模块。 - 在Node.js中,采用了CommonJS规范,通过
require
函数加载模块,通过module.exports
导出模块。
- CommonJS规范定义了
- AMD规范:在浏览器端,由于网络请求的异步性质,CommonJS规范在加载模块时存在一些问题。为了解决这个问题,AMD(Asynchronous Module Definition)规范在2011年提出,它引入了
define
和require
函数,使得模块的加载可以异步进行。RequireJS是一个遵循AMD规范的模块加载器。 - UMD规范:由于CommonJS和AMD规范在语法和用法上存在差异,为了兼容两者,UMD(Universal Module Definition)规范在2013年提出。UMD规范允许开发者编写兼容CommonJS和AMD规范的模块,使得模块可以在不同的环境中使用。
- ES6模块规范:随着ES6(ECMAScript 2015)的发布,JavaScript语言本身引入了官方的模块化规范,即ESM规范。ESM规范在语法和用法上与CommonJS和AMD规范有所不同,它使用
export
和import
语句来导出和导入模块。ESM规范提供了更强大和灵活的模块化功能,并逐渐成为JavaScript的主流模块规范。
总结来说,JavaScript模块规范经历了CommonJS、AMD、UMD等发展阶段,最终在ES6中引入了官方的ESM规范。这些规范的出现和发展,为JavaScript开发者提供了不同的模块化方案,使得代码的组织和复用更加方便和灵活。
作为Node.js开发者来说,我们只需要了解Commonjs(上一代)和ESM(当前Web规范)二种规范即可,甚至可以只会ESM,短期内也是够用的。
CommonJS
CommonJS是一种用于JavaScript模块化的规范,它定义了一种模块加载和导出机制,使得开发者可以将代码组织成独立的模块,并在需要时进行加载和使用。
在CommonJS规范中,每个模块都是一个单独的文件,文件中的代码通过module.exports
导出,其他模块可以通过require
函数来加载和使用这些导出的模块。
以下是一个使用CommonJS规范的简单示例:
假设有两个文件,分别为math.js
和app.js
。
math.js文件中定义了一个简单的加法函数:
// math.js
function add(a, b) {
return a + b;
}
module.exports = add;
app.js文件中使用了math.js中导出的加法函数:
// app.js
const add = require('./math.js');
console.log(add(2, 3)); // 输出:5
在app.js中,我们使用了require
函数来加载math.js文件,并将导出的add函数赋值给变量add
。然后,我们可以直接调用add
函数进行加法运算。
这是一个简单的CommonJS规范的示例,通过模块的导出和加载,我们可以将代码组织成独立的模块,提高代码的可维护性和复用性。
市面上很多教程里的代码都是下面这样采用CommonJS规范实现的,这是因为当下市面上能够见到的书籍基本都是基于Node.js 8版本之前编写的。
const fs = require('fs');
fs.writeFile('example.txt', 'Hello, world!', 'utf8', (error) => {
if (error) {
console.error(error);
} else {
console.log('File written successfully.');
}
});
在Node.js v20里,它也是可以运行的,只是不推荐而已。有更先进且符合Web标准的ESM规范,了解一下就够,不必深学(以前为了动态加载模块,很多时候还会处理require.cache,比如jest里的测试Node.js有时候会有一些莫名奇妙的bug,就是这个原因)。
ESM
ESM(ECMAScript Modules)是JavaScript官方的模块化规范,它最早于2015年在ES6(ECMAScript 2015)中引入。ESM规范在语法和用法上与CommonJS和AMD规范有所不同,它使用export
和import
语句来导出和导入模块。
在ES6中,ESM规范仅仅是一个提案,需要通过Babel等工具进行转换才能在浏览器和Node.js中使用。随着时间的推移和ES6的广泛应用,ESM规范逐渐成为JavaScript的主流模块规范。
自从2021年 sindresorhus 发表 esm only 的宣言 一年多以来,许多项目开始转向了 esm only,即仅支持 esm 而不支持 cjs,以此来迫使整个生态更快的迁移到 esm only。
一些流行的项目已经这样做了
- sindresorhus 维护的上千个 npm 包
- node-fetch
- remark 系列
- 更多开源模块。。。
我是非常支持sindresorhus的做法,规范是不断演进的,背着包袱前行,不过是权宜之计。
export和import
在Node.js中,ESM规范最早于2017年在v8.5.0版本中引入(狼书卷一中讲过esnext,甚至还有systemjs这样的esm加载器,如今浏览器内置,用武之地极小),但是需要在文件中添加"type": "module"
的声明才能启用ESM模块。随着Node.js的版本更新,ESM规范的支持逐渐完善,可以通过import
和export
语句来导入和导出模块,也可以使用动态导入等高级特性。
在ESM规范中,每个模块都是一个单独的文件,文件中的代码通过export
和import
语句来导出和导入模块。
1、export
export 用于对外输出模块,可导出常量、函数、文件等,相当于定义了对外的接口,两种导出方式:
- export: 使用 export 方式导出的,导入时要加上 {} 需预先知道要加载的变量名,在一个文件中可以使用多次。
- export default: 为模块指定默认输出,这样加载时就不需要知道所加载的模块变量名,一个文件中仅可使用一次。
// src/caculator.mjs
export function add (a, b) {
return a + b;
};
export function subtract (a, b) {
return a - b;
}
const caculator = {
add,
subtract,
}
export default caculator;
2、import
import 语句用于导入另一个模块导出的绑定,三种导入方式:
- 导入默认值:导入在 export default 定义的默认接口。
- as 别名导入:在导入时可以重命名在 export 中定义的接口。
- 单个或多个导入:根据需要导入 export 定一个的一个或多个接口。
// src/app.mjs
import caculator from "./src/caculator.mjs";
import * as caculatorAs from "./src/caculator.mjs";
const result = caculator.add(4, 2);
console.dir(result);
// caculator.subtract(4, 2);
const result2 = caculatorAs.subtract(4, 2);
console.dir(result2);
我们使用了import
语句来加载math.js文件中导出的add
函数,并将其赋值给变量add
。然后,我们可以直接调用add
函数进行加法运算。这是一个简单的ESM规范的示例,通过模块的导出和加载,我们可以将代码组织成独立的模块,提高代码的可维护性和复用性。
ESM执行的2种方式
需要注意的是ESM在Node.js世界里有2种执行方式。
1、在Node.js中,ESM规范需要使用.mjs
文件扩展名,如果在package.json文件中没有指定"type": "module"
,就必须使用.mjs
文件扩展名。
$ node src/app.mjs
2、使用.js
文件扩展名,需要在package.json文件中指定"type": "module"
,
$ node app.js
这种用法在Node.js v17以上版本都可以直接使用。
在命令行中如果Node.js版本大于8或小于17,你也可以使用--experimental-modules
参数来启用ESM模块,在Node.js v17版本以上已经是默认开启ESM模块了。
$ node --experimental-modules app.js
说明。
- 第一种方式,最为原始,最开始实现loader的时候,为了区别CommonJS和ESM而做区分使用的文件扩展
- 第二种方式,通过确定当前模块是ESM模块,你就可以不用使用文件后缀名来区分它是什么格式,很明显这是更简单的。
node: 引用
在Node.js v12.20之后就已经开始支持URLs引用了。通过node:可以引用Node.js内置的模块,简言之,Node.js 内置的SDK方法都可以通过这种方式来引用。
举个例子,参见src/buildin-modules.mjs
import { builtinModules as builtin } from "node:module";
console.dir(builtin);
执行结果如下。
这些其实就是Node.js SDK里所有模块,需要说明的是以”_“开头的是内部私有模块,不要直接使用,比如”_http_agent“是在”http“模块里应用的,对应的功能有对外导出。
Async/await
理解了ESM规范中的export和import,以及import xx from ‘node:xx’方式,你已经可以开始写Node.js v20的项目了。但Node.js最核心的点其实是异步流程控制,如果处理不好异步流程控制,在Node.js世界里,你就只能写写Cli工具,能做的相对比较有限。
本节,我们之所以以Async/await命名,是因为它是你在Node.js异步世界里必须掌握的内容,它和ESM一样重要。
Node.js SDK Api演进过程
为了让大家更好的理解为什么Async/await如此重要,我们需要先讲一下Node.js SDK API风格演示过程。
作为Node.js开发者,以下是一些常见的Node.js SDK写法风格:
- 回调函数(Callback)风格:
- 在早期的Node.js版本中,常见的异步操作是通过回调函数来处理的。
- 回调函数通常有两个参数,第一个参数用于传递错误信息,第二个参数用于传递结果或数据。
- 开发者需要在回调函数中处理错误和结果,以确保代码的正确执行。
- Promise风格:
- 随着ES6的普及,Promise成为了处理异步操作的一种新方式。
- Promise是一种表示异步操作的对象,可以通过
.then
和.catch
方法进行链式调用。 - 开发者可以使用
new Promise
来创建Promise对象,并在异步操作完成后调用resolve
或reject
来处理结果和错误。
- async/await风格:
- ES8引入了async/await语法糖,使得异步代码的书写更加简洁和直观。
- 使用
async
关键字定义一个异步函数,其中可以使用await
关键字等待一个Promise的完成。 - 开发者可以像编写同步代码一样编写异步代码,提高了代码的可读性和可维护性。
总结来说,Node.js SDK的写法风格在异步流程调用方面经历了从回调函数到Promise再到async/await的发展历程。开发者可以根据自己的喜好和项目需求选择合适的写法风格,以提高代码的可读性和可维护性。
异步流程控制概览
关于异步流程调用的发展历史,在《狼书1》第七章异步流程控制一章中,总结的已经非常全面了,如下图。
说明如下。
- callback hell:在esm出现之前error-first callback是默认API风格,所以会出现很多回调函数里嵌套回调的情况。在esm+promisify api之后,基本很少有人用error-first callback写法的api了,虽然Node.js会一直提供。
- Thunk已经退出历史舞台了,今天知道且用的人已经很少了。
- Generator在遍历和操作数据集合时候偶尔会用,更有Async Generator,我目前只在 https://github.com/typicode/xv 里见过。
在Node.js v20项目里,你需要掌握的是只有2个异步流程控制知识点:Promise和Async函数,就足够开发Node.js项目了。
同步还是回调?
Node.js本身以异步著名,绝大部分任务都是需要在EventLoop里运行,但也有意外,比如writeFile
和writeFileSync
就是特例,它们都是用于写入文件的两个不同的函数,具体用法行的差异如下。
1、writeFile
是一个异步函数,它接受文件路径、要写入的数据和可选的编码参数,并在写入完成后调用回调函数。这意味着在写入文件时,程序可以继续执行其他操作,而不必等待写入完成。示例代码如下:
const fs = require('fs');
fs.writeFile('example.txt', 'Hello, world!', 'utf8', (error) => {
if (error) {
console.error(error);
} else {
console.log('File written successfully.');
}
});
2、writeFileSync
是一个同步函数,它接受文件路径、要写入的数据和可选的编码参数,并在写入完成后返回。这意味着在写入文件时,程序会阻塞并等待写入完成,然后继续执行后续操作。示例代码如下:
const fs = require('fs');
try {
fs.writeFileSync('example.txt', 'Hello, world!', 'utf8');
console.log('File written successfully.');
} catch (error) {
console.error(error);
}
总的来说,writeFile
适用于异步编程场景,可以在写入文件的同时执行其他操作,而writeFileSync
适用于同步编程场景,需要等待写入完成后再执行后续操作。选择使用哪个函数取决于具体的应用场景和需求。
讲这个的目的是为了说明原理。
但Sync方法是由场景限制的,不可以乱用。
反思
Promise
Promise是JS异步编程中的重要概念,异步抽象处理对象,是目前比较流行Javascript异步编程解决方案之一。在Node.js世界里,本身是Error-first Callback写法,和Promise结合是最简单的用法。所以在Node.js 10之前,基本上都会使用bluebird这样的Promise库来实现,后面ES6内置Promise,所以Node.js(基于v8内核,v8是Chrome的js渲染引擎)也开始支持Promise对象,并且对fs等模块进行了Promise化,甚至还出现了util.promisify
这样的工具函数。
ES6 Promise
ES6引入了Promise对象,它是一种用于处理异步操作的对象。Promise可以将异步操作转化为类似同步操作的链式调用方式,使得代码更易读、更易维护。
Promise对象具有以下特点:
- Promise对象是一个构造函数,通过
new Promise()
来创建一个Promise实例。 - Promise对象的构造函数接受一个函数作为参数,该函数包含两个参数:resolve和reject。resolve用于将Promise状态从pending转为fulfilled,reject用于将Promise状态从pending转为rejected。
- Promise对象的实例具有
then
方法,用于指定Promise状态变为fulfilled时的回调函数,并返回一个新的Promise对象。then
方法可以被链式调用,即可以在一个then
方法的回调函数中再调用另一个then
方法。
下面是一个简单的示例代码,演示了Promise的基本用法:
// 创建一个Promise对象
const promise = new Promise((resolve, reject) => {
// 模拟异步操作
setTimeout(() => {
const randomNumber = Math.random();
if (randomNumber > 0.5) {
// 异步操作成功,将Promise状态从pending转为fulfilled,并返回结果
resolve(randomNumber);
} else {
// 异步操作失败,将Promise状态从pending转为rejected,并返回错误信息
reject(new Error('Random number is less than 0.5'));
}
}, 1000);
});
// 使用then方法指定Promise状态变为fulfilled时的回调函数
promise.then(result => {
console.log('Promise fulfilled:', result);
}).catch(error => {
console.error('Promise rejected:', error);
});
在上面的示例中,我们首先创建了一个Promise对象,通过new Promise()
来定义一个异步操作。在异步操作中,通过setTimeout
模拟了一个耗时1秒的操作,并根据随机数的大小决定操作成功还是失败。如果随机数大于0.5,操作成功,调用resolve
函数将Promise状态从pending转为fulfilled,并返回随机数;如果随机数小于等于0.5,操作失败,调用reject
函数将Promise状态从pending转为rejected,并返回一个错误对象。
然后,我们使用then
方法指定了Promise状态变为fulfilled时的回调函数,通过链式调用的方式,可以在then
方法的回调函数中继续调用另一个then
方法。
最后,我们通过catch
方法指定了Promise状态变为rejected时的回调函数,用于处理异步操作失败的情况。
综上所述,ES6 Promise是一种用于处理异步操作的对象,通过将异步操作转化为类似同步操作的链式调用方式,使得代码更易读、更易维护。Promise具有三种状态:pending、fulfilled和rejected,可以通过resolve
和reject
函数将Promise状态从pending转为fulfilled或rejected,并返回相应的结果或错误信息。then
方法用于指定Promise状态变为fulfilled时的回调函数,catch
方法用于指定Promise状态变为rejected时的回调函数。
Promisify
如果不用Sync方法,也不想使用回调函数写法,使用Promise是一个更好的选择。在 Node.js v10开始,他们在使用 Promisepromises
的fs
模块中创建了一个对象,而主fs
模块继续公开使用回调的函数。在此程序中,你可以将导入模块的 promise 版本。
- 以前导入模块后,创建一个异步函数来读取文件。异步函数学习bluebird方式,以
async
关键字开头。现在和fs保持一致了,更方便。
使用异步函数,您可以使用await
关键字解析承诺,而不是将承诺与.then()
方法链接起来。
fs.promises
模块提供了一组以Promise风格的方式封装的文件系统函数,可以方便地处理文件操作。以下是一个使用fs.promises
模块的示例:
const fs = require('fs').promises;
// 使用Promise风格的函数读取文件内容
fs.readFile('example.txt', 'utf8')
.then(data => {
console.log(data);
})
.catch(error => {
console.error(error);
});
// 使用Promise风格的函数写入文件内容
fs.writeFile('example.txt', 'Hello, world!', 'utf8')
.then(() => {
console.log('File written successfully.');
})
.catch(error => {
console.error(error);
});
在上面的示例中,我们首先引入了fs.promises
模块,然后使用readFile
函数读取文件内容,并使用writeFile
函数写入文件内容。这些函数都返回Promise对象,因此我们可以使用.then
和.catch
方法处理异步操作的结果和错误。
需要注意的是,fs.promises
模块仅在Node.js v10及以上版本中可用。如果你使用的是Node.js v8及以下版本,可以使用util.promisify
方法将其他异步函数转换为Promise风格的函数,然后使用该函数进行文件操作。
综上所述,通过使用fs.promises
模块,我们可以方便地处理文件操作,并使用Promise风格的函数处理异步操作。
util.promisify
在Node.js中,可以使用util.promisify
方法将遵循错误优先回调的函数转换为返回Promise的函数,从而简化异步操作的处理。在文件系统模块(fs)中,可以使用promisify
方法将异步的文件操作函数转换为Promise风格的函数。
以下是一个使用fs.promises
模块(Node.js v10及以上版本)和util.promisify
方法的示例:
const fs = require('fs');
const { promisify } = require('util');
// 使用promisify方法将fs.readFile函数转换为Promise风格的函数
const readFileAsync = promisify(fs.readFile);
// 使用Promise风格的函数读取文件内容
readFileAsync('example.txt', 'utf8')
.then(data => {
console.log(data);
})
.catch(error => {
console.error(error);
});
在上面的示例中,我们首先引入了fs
模块和util
模块的promisify
方法。然后,我们使用promisify
方法将fs.readFile
函数转换为返回Promise的函数readFileAsync
。最后,我们使用readFileAsync
函数来读取文件内容,并通过.then
和.catch
方法处理异步操作的结果和错误。
需要注意的是,上述示例使用了Node.js v10及以上版本中引入的fs.promises
模块,该模块提供了一组以Promise风格的方式封装的文件系统函数。如果你使用的是Node.js v8及以下版本,可以使用util.promisify
方法将其他异步函数转换为Promise风格的函数,然后使用该函数进行文件操作。
综上所述,通过使用util.promisify
方法,我们可以将Node.js中的异步函数转换为Promise风格的函数,从而更方便地处理异步操作。
Async函数
ES6中的async函数是一种异步编程的解决方案,它使得异步操作更加简洁明了,并且可以避免回调地狱的问题。async函数本质上是一个返回Promise对象的函数,可以使用await关键字来等待Promise对象的状态变化。
async函数具有以下特点:
- async函数声明时需要在函数前面添加async关键字,表示该函数是一个异步函数。
- async函数内部可以使用await关键字来等待Promise对象的状态变化,await关键字后面跟上一个Promise对象,表示等待该Promise对象的状态变为fulfilled或rejected。
- async函数内部可以包含多个await关键字,这些异步操作将按照顺序依次执行。
- async函数返回一个Promise对象,该对象的状态由async函数内部的异步操作决定,如果异步操作成功,则Promise状态为fulfilled,并返回异步操作的结果;如果异步操作失败,则Promise状态为rejected,并返回错误信息。
下面是一个简单的示例代码,演示了async函数的基本用法:
// 定义一个异步函数
async function main() {
console.dir("hi async function");
try {
// 调用Promise函数
await fn();
} catch (error) {
console.log(error);
}
}
// 调用Promise函数
function fn() {
return new Promise(function (resolve, reject) {
console.dir("hi promise function");
resolve();
});
}
// 调用异步函数
main();
在上面的示例中,我们首先定义了一个异步函数main
,该函数使用了async关键字来声明,表示该函数是一个异步函数。在函数内部,我们使用了await关键字来等待异步操作完成,首先使用await fn函数来执行fn函数中的Promise方法。最后,我们在最下面调用main方法来获得异步操作的结果。
综上所述,ES6中的async函数是一种异步编程的解决方案,它使得异步操作更加简洁明了,并且可以避免回调地狱的问题。async函数使用async关键字来声明,内部可以使用await关键字来等待异步操作完成,返回一个Promise对象,该对象的状态由异步操作的结果决定。
第一个Node.js v20项目
学习任何技术,最好的方式都是从Helloworld开始,能够把Helloworld做到极致,标准,内聚,其实也是非常难的。
下面我们就来一起看一下第一个Node.js v20项目如何编写吧。
要点
推荐做法,能使用现代Web规范的地方尽量使用。
1、使用ESM规范,作为模块加载方案,掌握import和export就可以
2、使用import xx from ‘node:xxx’调用
3、配置package.json中的"type": "module",使用.js后缀进行开发
4、使用Async函数作为异步流程方案,如果必须要使用Promise
初始化项目
通过npm init -y创建项目
这是npm默认创建的package.json,此时并没有配置ES模块信息。需要手动编写,增加"type": "module"。此时,package.json文件内容如下。
{
"name": "helloworld",
"version": "1.0.0",
"description": "",
"type": "module",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
创建index.js
为了演示方便,我们采用之前的代码。
// 定义一个异步函数
async function sayHi(name) {
try {
// 调用Promise函数
const text = await helloworld(name);
console.log(text);
} catch (error) {
console.log(error);
}
}
// 调用Promise函数
function helloworld(name) {
return new Promise(function (resolve, reject) {
resolve(`Hello ${name}!`);
});
}
// 调用异步函数
const person = process.argv[2];
sayHi(person);
执行如下。
$ node index.js alfred
'Hello alfred!'
参考
- https://nodejs.dev/en/learn/run-nodejs-scripts-from-the-command-line/
- https://github.com/75lb/command-line-args
- https://github.com/75lb/command-line-usage
发布npm
前置条件是npmjs.com上注册并登录.
修改package.json如下
{
"name": "node-v20-helloworld",
"version": "1.0.5",
"description": "",
"type": "module",
"main": "index.js",
"bin": {
"node-v20-helloworld": "index.js"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "npmtudy",
"license": "ISC",
"files": [
"index.js"
],
"engines": {
"node": "^20"
}
}
说明
- bin 是配置cli名称的配置。
- files 是发布的npm包里包含的内容,比如测试之类的只在开发阶段使用,真正的npm包里可以移除掉。
- engines 用于限制node版本,比如这里的配置就是Node.js v20以上才能安装。
然后执行npm publish就可以正常发布,如果不能发布,可以通过npm verion进行调整,比较常用的就是修复问题,通过patch来修改最后一位的版本号。
$ npm version patch
v1.0.6
测试
Node.js诞生自2009年,在v18之前的13年时间里都没有内置任何测试框架。一直都是使用npm生态。像本书系列卷三中提到的几个测试框架,都已经有5年以上的历史了。
测试框架 | 当前主要版本 | 年限 |
---|---|---|
mocha | v10 | 11 |
tap | v16 | 11 |
tape | v5 | 10 |
ava | v5 | 9 |
jest | v27 | 7 |
Node.js遵循与JavaScript本身相同的"最小核心"原则。因此,像代码检查工具、代码格式化工具和测试运行器这样的工具最好作为第三方工具提供。虽然这是一个很好的想法很长一段时间,但现在没有标准测试工具的任何语言都显得有些奇怪。Deno、Rust和Go - 它们都有自己内置的测试运行器。
在Node.js v18开始内置了测试框架,在Node.js v20版本中,已经被标记为Stable能力,大家可以放心使用。
使用Node.js 内置的测试框架,测试代码如下。
import { test } from "node:test";
import assert from "node:assert";
import { sayHi } from "./index.js";
test("test if works correctly", function (t) {
const log = t.mock.method(global.console, "log");
assert.strictEqual(log.mock.callCount(), 0);
// call hello world say method
sayHi("liangqi");
assert.strictEqual(log.mock.callCount(), 1);
});
在package.json中修改npm scripts
"scripts": {
"test": "node --test"
},
执行测试结果如下。
$ npm test
> node-v20-helloworld@1.0.6 test
> node --test
Hello liangqi!
✖ test if works correctly (1.703916ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
0 !== 1
at TestContext.<anonymous> (file:///Users/alfred/workspace/npmstudy/node-v20-helloworld/index.test.js:13:10)
at Test.runInAsyncScope (node:async_hooks:206:9)
at Test.run (node:internal/test_runner/test:581:25)
at Test.start (node:internal/test_runner/test:494:17)
at startSubtest (node:internal/test_runner/harness:207:17) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: 0,
expected: 1,
operator: 'strictEqual'
}
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 59.11425
✖ failing tests:
✖ test if works correctly (1.703916ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
0 !== 1
at TestContext.<anonymous> (file:///Users/alfred/workspace/npmstudy/node-v20-helloworld/index.test.js:13:10)
at Test.runInAsyncScope (node:async_hooks:206:9)
at Test.run (node:internal/test_runner/test:581:25)
at Test.start (node:internal/test_runner/test:494:17)
at startSubtest (node:internal/test_runner/harness:207:17) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: 0,
expected: 1,
operator: 'strictEqual'
}
竟然报错了!这就很莫名奇妙,方法调用都是对的,断言也没问题。
后经过排查,发现sayHi是Async函数,在测试方法里,没有使用await来对接。需要修改2处。
- test("test if works correctly", async function (t) {}),第二个参数,需要修噶以为Async函数,这是因为await外层必须是async函数。
- sayHi("liangqi") 方法需要改成await sayHi("liangqi"),这样异步方法就转换为同步方法了。
将代码修改如下
import { test } from "node:test";
import assert from "node:assert";
import { sayHi } from "./index.js";
test("test if works correctly", async function (t) {
const log = t.mock.method(global.console, "log");
assert.strictEqual(log.mock.callCount(), 0);
// call hello world say method
await sayHi("liangqi");
assert.strictEqual(log.mock.callCount(), 1);
});
此时,执行npm test
$ npm test
> node-v20-helloworld@1.0.6 test
> node --test
Hello liangqi!
✔ test if works correctly (1.092375ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
至此,就完成了测试的基本步骤,只有CI/CD我们在后面进阶章节进行讲解。
模块用法
npm上的模块分2种。
- 普通模块,主要是为import from使用的。
- 二进制模块,主要是为了编写命令行Cli工具使用的。
下面分别进行演示。
方式1:通过二进制模块方式测试
$ npm i -g node-v20-helloworld
$ node-v20-helloworld liangqi
'Hello liangqi!'
方式2:代码调用
$ npm i --save node-v20-helloworld
调用代码如下,一定要注意await,上面测试部分有见过坑,不可偷懒。
#! /usr/bin/env node
import { sayHi } from 'node-v20-helloworld';
async function main(){
// 调用异步函数
const person = process.argv[2];
await sayHi(person);
}
main();
小结
本章主要是讲解了创建一个Node.js v20的入门项目,需要了解的知识点。看似简单,想要真的写好一个Hello World,竟然也需要上万字的教程,相信你已经了解它的难度。
- ESM用法必须掌握,使用
node:
引用必须掌握,CommonJS了解即可。 - Async必须掌握,Promise可以掌握,Error-first callback了解即可。
- Node.js运行原理和新特性了解即可。
- 3m安装法建议掌握。
- 发布npm模块建议掌握。
现在,你已经掌握了Node.js v20项目开发基础,接下来我们继续学习如何使用TS开发Node.js项目吧。
源码:https://github.com/npmstudy/node-v20-helloworld/tree/main