如期而至的Node.js V20.0:揭秘9大新特性!

发表时间: 2023-04-20 05:20

大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!

高级前端进阶

今天给大家带来的主题是Node.js V20.0.0的发布,话不多说,直接开始!

1.理解 Node.js 发版规律

1.1 什么 Semver

Sem + ver, Sem 是 Semantic(语义的)之意,ver 是 Version(版本)之意。整体表示一种版本命名的方式,要体现一定的版本变更内容的含义。目前的 Semver 一般指由 Semver.org 组织制定的版本规范。其基本要求如下:版本号由 MAJOR.MINOR.PATCH 3 部分组成,每个部分的含义如下:

  • MAJOR: 软件的 API 改变
  • MINOR: 修改或添加功能,但保持向后兼容(API 未变)
  • PATCH:补丁,主要是错误修复

1.2 LTS(Long Term Support)与 Current 含义

关于 Node.js 的发布流程,需要记住以下几个要点:

  • 每 6 个月发布一个主要版本(即 Major 版本),作为 Current 版本,该版本可能是奇数版本也可能是偶数版本。但是,因为其是最新的,所以包含 Node.js 平台的最新特性
  • 4 月发布偶数版本,10 月发布奇数版本
  • 每 12 个月将一个版本转入 LTS 版本,LTS 代表了一个被不断修正与改进的版本。
  • LTS 版本要经历两个阶段:活跃期(Active)与维护期(Maintenance)
  • 活跃期共 18 个月,即在不改变兼容性的情况下,周期性修改 Bug 并合并其他版本的重要修改
    • (1)每个时刻 Current 版本只有一个,LTS 版本可能有 3 个,LTS-Active 版本可能有 2 个。在 Node.js 官网中给出的 LTS 版本总是处于 LTS 的最新版本
    • (2)该版本处于活跃期(Ative)时,可以看到其 MINOR 版本在不断升级
  • 维护期共 12 个月,负责修改当前版本的 Bug 以及特别紧急问题的修复,如安全性问题。

下面是 Node 官网的最新信息,即最新 Current 版本 V20.0.0、而 LTS 版本为 V18.16.0 版。

2.Node.js V20.0.0 新特性概览

Node.js 20 如期发布,带来的诸多特性包括:新的 Node.js 权限模型、同步 import.meta.resolve、稳定的 test_runner、V8 JavaScript 引擎更新至 V11.3、Ada 更新至 2.0 等等!

Node.js 项目在多个领域取得重要进展,许多新功能和修复被集成进现有的 LTS 版本。而 Node.js 20 的变更日志中概述的更改仅代表自上一个主要版本以来功能变更的一小部分。 这篇博文将在与这些变化相关的更大范围的工作中添加一些额外的背景补充信息。

需要注意的是,Node.js 20 将于 10 月进入长期支持 (LTS),但在此之前,V20 将是未来六个月的当前版本。 Node.js 官方鼓励开发者探索这个最新版本提供的新功能和诸多优势,并评估对现有应用程序的潜在影响。

3.细数 Node.js V20 的诸多新特性

3.1 权限模型

Node.js 权限模型是一种实验性机制,用于在执行期间限制对特定资源的访问。在包含权限模型的第一个版本中,主要包括以下核心功能:

  • 使用 --allow-fs-read 和 --allow-fs-write 限制对文件系统的访问(读写)
  • 使用 --allow-child-process 限制对 child_process 的访问
  • 使用 --allow-worker 限制对 worker_threads 的访问
  • 可用权限由 --experimental-permission 标志记录限制对本机插件的访问(与 --no-addons 标志相同)

使用 --experimental-permission 启动 Node.js 时,访问文件系统、生成进程(Spawn Processes)和使用 node:worker_threads 的能力将受到限制。

通过引入 --allow-fs-read 和 --allow-fs-write 标志,使用 Node.js 的开发人员可以更好地控制文件系统访问权限。 这些实验性功能允许开发者更精细地控制 Node.js 进程可以访问文件系统的哪些部分。

要启用这些标志,开发人员可以使用 --experimental-permission 标志以及所需的权限。 例如,运行以下命令允许对整个文件系统进行读写访问:

$ node --experimental-permission --allow-fs-read=* --allow-fs-write=* index.js

开发人员可以通过将逗号分隔值传递给标志来指定文件系统访问的特定路径。例如,以下命令允许对 /tmp/ 文件夹进行写访问:

$ node --experimental-permission --allow-fs-write=/tmp/ --allow-fs-read=/home/index.js index.js

通配符模式也可用于允许一次访问多个文件或文件夹。例如,以下命令允许对 /home/ 目录中以 test 开头的所有文件和文件夹进行读取访问:

$ node --experimental-permission --allow-fs-read=/home/test* index.js

启用权限模型后,流程对象的新权限属性可用于检查是否在运行时授予了某个权限。

process.permission.has('fs.write');// 返回trueprocess.permission.has('fs.write', '/home/nodejs/protected-folder');// 返回true

请务必注意,这些功能仍处于试验阶段,可能会在 Node.js 的未来某个版本中发生变化 。通过权限精细化控制的引入, Node.js 在未来将会更加安全。

3.2 自定义 ESM 加载程序 Hooks 稳定

通过加载程序 (--experimental-loader=./foo.mjs) 提供的自定义 ES 模块生命周期钩子现在在与主线程隔离的专用线程中运行。 这为加载程序提供了一个单独的 scope,并确保加载程序和应用程序代码之间没有交叉污染。

import.meta.resolve() 是在 JavaScript 模块的 import.meta 对象上定义的内置函数,它使用当前模块的 URL 作为基础将模块说明符解析为 URL。import.meta.resolve() 允许脚本访问名称的模块说明符解析算法,如下所示:

// Script at https://example.com/main.jsconst helperPath = import.meta.resolve('./lib/helper.js');console.log(helperPath);// "https://example.com/lib/helper.js"

请注意, import.meta.resolve() 仅执行解析,不会尝试加载或导入结果路径。 因此,无论返回的路径是否对应于一个存在的文件,也不管该文件是否包含模块的有效代码,它的返回值都是相同的。

它不同于动态导入,因为虽然两者都接受模块说明符作为第一个参数,但 import.meta.resolve() 返回将被导入的路径,而不会尝试访问该路径。 因此,以下两个实际上是相同的代码:

// 方法 1console.log(await import('./lib/helper.js'));// 方法 2const helperPath = import.meta.resolve('./lib/helper.js');// 即使无法成功导入./lib/helper.js,这里的代码也不会抛出错误,直到第 2 行执行导入。console.log(await import(helperPath));

与浏览器行为一致,Node.js 的 import.meta.resolve() 现在同步返回。但是请注意,如果加载程序开发者需要,用户加载程序中的解析 Hooks 可以保持异步,并且 import.meta.resolve 仍将在应用程序代码中同步返回。

这些更改是将 ESM 加载程序标记为稳定之前的最后一项未完成的项目,一段时间后如果社区没有报告重大错误,Node.js 官方打算将加载器标志、import.meta.resolve 以及 resolve 和 load 钩子标记为稳定。

3.3 集成 V8 11.3 版本

Node.js 中包含一个新版本的 V8 引擎,即更新到版本 V11.3,也是 Chromium 113 的一部分。V8 11.3 带来了诸多性能改进和新的语言功能,包括:

  • String.prototype.isWellFormed 和 toWellFormed
  • 通过复制改变 Array 和 TypedArray 的方法
  • 可调整大小的 ArrayBuffer 和可增长的 SharedArrayBuffer
  • 带有集合符号的 RegExp v 标志 + 字符串的属性
  • WebAssembly 尾调用

以 isWellFormed 和 toWellFormed 为例。String 值的 toWellFormed() 方法返回一个字符串,其中该字符串的所有单独代理项都替换为 Unicode 替换字符 U+FFFD,比如:

const strings = [  // Lone high surrogate  'ab\uD800',  'ab\uD800c',  // Lone low surrogate  '\uDFFFab',  'c\uDFFFab',  // Well-formed  'abc',  'ab\uD83D\uDE04c',];for (const str of strings) {  console.log(str.toWellFormed());}// Logs:// "ab�"// "ab�c"// "�ab"// "c�ab"// "abc"// "abc"

String 值的 isWellFormed() 方法返回一个布尔值,指示此字符串是否包含任何单独的代理项。

const illFormed = 'https://example.com/search?q=\uD800';try {  encodeURI(illFormed);} catch (e) {  console.log(e); // URIError: URI malformed}if (illFormed.isWellFormed()) {  console.log(encodeURI(illFormed));} else {  console.warn('Ill-formed strings encountered.');  // Ill-formed strings encountered.}

尾调用是一种程序开发技巧,主要在函数程序语言中使用,借此提高递归函数的效率,尾调用是指在函数最后一个操作调用另一个函数,但在调用完成之后不做任何额外的操作。

3.4 稳定的测试运行器

Node.js 版本 20 的更新包括对 test_runner 模块的重要更改。 在最近的更新后,该模块已被标记为稳定。 稳定的测试运行器可用于编写和运行测试的构建块,包括:

  • describe, it/test 和 hooks 来构建测试文件
  • mocking
  • watch 模式
  • node --test 用于并行运行多个测试文件

测试运行器还包括一些还不稳定的部分,包括:reporters 和代码覆盖率。下面是一个使用测试运行器的简单示例:

import { test, mock } from 'node:test';import assert from 'node:assert';import fs from 'node:fs';mock.method(fs, 'readFile', async () => 'Hello World');test('synchronous passing test', async (t) => {  // 这个测试通过了,因为它没有抛出异常。  assert.strictEqual(await fs.readFile('a.txt'), 'Hello World');});

3.5 性能优化

自上一个主要版本以来,Node.js 专门新成立了性能团队。 Node.js 20 对运行时的基本部分进行了许多改进,包括 URL、fetch() 和 EventTarget。

EventTarget 和 Event 对象是 EventTarget Web API 的特定于 Node.js 的实现,由一些 Node.js 核心 API 公开。比如下面的代码示例:

const target = new EventTarget();target.addEventListener('foo', (event) => {  console.log('foo event happened!');});

初始化 EventTarget 的成本减少了一半,从而可以更快地访问使用它的所有子系统。 此外,已利用 V8 快速 (Fast)API 调用来提高 API 的性能,例如 :URL.canParse() 和计时器。

// URL.canParse()检查是否可以将相对于`base`的`input`解析为 URL,第一个参数为input,第二个为baseconst isValid = URL.canParse('/foo', 'https://example.org/');// trueconst isNotValid = URL.canParse('/foo');// false

Node.js 20 包括特定的更改,例如 :引入 Ada 的更新版本 2.0,这是一种用 C++ 编写的快速且符合规范的 URL 解析器。

Node.js 目前正在努力通过重构来降低符合规范的成本,以消除对流、URL、URLSearchParams 和字符串解码器的验证检查,从而更加符合规范。

3.6 准备单个可执行应用程序现在需要注入一个 Blob

Node.js 在过去一年中一直致力于支持单一可执行应用程序 (Single Executable Applications,即 SEA),在 V 20 中得到了初步支持。 Node.js 团队将继续完善该方法,因为该功能仍处于实验阶段。

在 Node.js 20 中,构建单个可执行应用程序需要从 JSON 配置中注入由 Node.js 准备的 blob,而不是注入原始 JS 文件。比如 sea-config.json 文件:

{  "main": "hello.js",  "output": "sea-prep.blob"}

这会将 blob 写入 sea-prep.blob 文件。

$ node --experimental-sea-config sea-config.json

现在可以将此 blob 注入到二进制文件中。进行此更改是为了允许将多个共存资源嵌入到 SEA(单一可执行应用程序)中,从而开辟新的用例。

3.7 网络加密 API

该项目致力于与其他 JavaScript 环境的互操作性。 作为 Node.js 20 中的一个示例,Web Crypto API 函数的参数现在支持按照其 WebIDL 定义进行强制和验证,就像在其他 Web Crypto API 实现中一样。

const { subtle } = globalThis.crypto;(async function () {  // 与其他 Web Crypto API 实现一样,参数现在根据其 WebIDL 定义进行强制转换和验证。  const key = await subtle.generateKey(    {      name: 'HMAC',      hash: 'SHA-256',      length: 256,    },    true,    ['sign', 'verify']  );  const enc = new TextEncoder();  const message = enc.encode('I love cupcakes');  const digest = await subtle.sign(    {      name: 'HMAC',    },    key,    message  );})();

这进一步提高了与 Web Crypto API 的其他 JavaScript 实现的互操作性。

3.8 Windows 官方支持 ARM64

Node.js 拥有广泛的平台和架构支持,但是往往开发者希望它可以在任何地方运行。 Node.js 现在已经支持 ARM64 Windows 的二进制文件,允许在该平台上本地执行。

MSI、zip/7z 包和可执行文件可从 Node.js 下载站点以及所有其他平台获得。 CI 系统已更新,所有更改现在都在 ARM64 Windows 上进行了全面测试,以确保兼容性。

3.9 Web Assembly 系统接口(WASI)进展

Node.js V20 显著的进步是 Node.js 中的 WASI(WebAssembly System Interface) 实现,虽然是实验性的,但已不再需要命令行选项来启用 WASI。

WASI API 提供了 WebAssembly 系统接口规范的实现。 WASI 允许沙盒 WebAssembly 应用程序通过一组类似 POSIX 的函数访问底层操作系统

import { readFile } from 'node:fs/promises';import { WASI } from 'wasi';import { argv, env } from 'node:process';const wasi = new WASI({  version: 'preview1',  // 版本  args: argv,  env,  preopens: {    '/sandbox': '/some/real/path/that/wasm/can/access',  },});const wasm = await WebAssembly.compile(  await readFile(new URL('./demo.wasm', import.meta.url)));const instance = await WebAssembly.instantiate(wasm, wasi.getImportObject());wasi.start(instance);

注意,在 V20.x 版本中,版本是必需的,没有默认值。 这一点很重要,因为支持新版本的应用程序不会默认为可能已过时的版本。 但是,这确实意味着任何依赖版本默认值的代码都需要更新以请求特定版本。

4.本文总结

Node.js 14 将于 2023 年 4 月结束生命周期,因此官方非常建议开始计划升级到 Node.js 18 (LTS) 或 Node.js 20(即将成为 LTS)。

同时, Node.js 16 (LTS) 将于 2023 年 9 月结束生命周期,该生命周期从 2024 年 4 月开始提前,以配合 OpenSSL 1.1.1 的支持结束。

因为篇幅有限,文章并没有过多展开,如果有兴趣,文末的参考资料提供了优秀文档以供学习。最后,欢迎大家点赞、评论、转发、收藏!

参考资料

https://nodejs.org/en/blog/announcements/v20-release-announce

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta/resolve

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/isWellFormed

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toWellFormed

https://baijiahao.baidu.com/s?id=1762864725737291897&wfr=spider&for=pc

https://nodejs.org/api/events.html#eventtarget-and-event-api

https://nodejs.org/api/wasi.html

https://developer.okta.com/blog/2019/12/04/whats-new-nodejs-2020