2024 年你可以使用的十大 Node.js 現(xiàn)代特性
服務(wù)器端 JavaScript 運(yùn)行時(shí)進(jìn)來充滿了創(chuàng)新,例如 Bun 憑借兼容的 Node.js API 取得了長足進(jìn)步,而 Node.js 運(yùn)行時(shí)則進(jìn)一步提供了豐富的標(biāo)準(zhǔn)庫和運(yùn)行時(shí)功能。
時(shí)間進(jìn)入 2024 年,是時(shí)候了解 Node.js 運(yùn)行時(shí)所提供的最新特性和功能了。這樣做不僅是為了“與時(shí)俱進(jìn)”,更是為了利用現(xiàn)代 API 的力量來編寫更高效、性能更安全的代碼。
接下來我們將詳細(xì)探討每個(gè)開發(fā)人員在 2024 年都應(yīng)該開始使用的 10 項(xiàng)現(xiàn)代 Node.js 運(yùn)行時(shí)功能。
先決條件:Node.js LTS 版本
在開始探索這些現(xiàn)代功能之前,請確保您使用的是 Node.js LTS(長期支持)版本。在撰寫本文時(shí),最新的 Node.js LTS 版本是 v20.14.0。
使用以下指令,檢查 Node.js 版本:
$ node --version
v20.14.0
如果您當(dāng)前沒有使用 LTS 版本,可以考慮使用 fnm 或 nvm 等版本管理器在不同 Node.js 版本之間輕松切換。
Node.js 20 有哪些新功能?
以下我們將介紹 Node.js 最新版本中引入的一些新功能。有些是穩(wěn)定的,有些仍處于實(shí)驗(yàn)階段,還有一些在之前的版本中就已經(jīng)得到了支持,只不過你可能還還沒有聽說過。
討論的主題包括:
- Node.js 測試運(yùn)行器(test runner)
- Node.js 原生 mocking
- Node.js 原生測試覆蓋率
- Node.js 監(jiān)聽模式(watch mode)
- Node.js corepack[2]
- Node.js import.meta.file:訪問 __dirname 和 __file
- Node.js 原生計(jì)時(shí)器 Promise
- Node.js 權(quán)限模塊(permissions module)
- Node.js 策略模塊(policy module)
Node.js 測試運(yùn)行器
在 Node.js 引入原生測試運(yùn)行支持之前,你會(huì)用什么工具進(jìn)行測試呢?當(dāng)然,這里有一些流行的選項(xiàng):vitest、jest、mocha 或者 node-tap。
下面,我們將了解如何在開發(fā)工作流程中使用 Node.js 原生測試運(yùn)行器進(jìn)行測試。
首先,你需要將 Node.js 中的 test 模塊導(dǎo)入到測試文件中,如下所示:
import { test } from 'node:test';
使用 node:test 運(yùn)行單個(gè)測試
要?jiǎng)?chuàng)建單個(gè)測試,你可以使用 test 函數(shù),傳入測試的名稱和回調(diào)函數(shù)。回調(diào)函數(shù)是你定義測試邏輯的地方。
import { test } from "node:test";
import assert from "node:assert";
import { add } from "../src/math.js";
test("should add two numbers", () => {
const result = add(1, 2);
assert.strictEqual(result, 3);
});
test("should fail to add strings", () => {
assert.throws(() => {
add("1", "2");
});
});
要運(yùn)行此測試,請使用 node --test [測試文件] 命令:
node --test tests/math.test.js
Node.js 測試運(yùn)行程序可以自動(dòng)檢測并運(yùn)行項(xiàng)目中的測試文件。按照約定,這些文件應(yīng)以 .test.js 結(jié)尾,但這個(gè)約定并不需要嚴(yán)格遵守。
如果省略測試文件位置參數(shù)(positional argument),那么 Node.js 測試運(yùn)行程序?qū)?yīng)用一些啟發(fā)式和 glob 模式匹配來查找測試文件——例如 test//tests/ 中的所有文件,或是帶有 test- 前綴或 .test 后綴的文件夾或文件。
你還可以通過 glob 語法匹配測試文件:
node --test '**/*.test.js'
通過 node:assert 使用斷言
Node.js 測試運(yùn)行程序通過內(nèi)置的 assert 模塊支持?jǐn)嘌?。你可以使?assert.strictEqual 等不同方法來驗(yàn)證測試結(jié)果。
import assert from 'node:assert';
test('Test 1', () => {
assert.strictEqual(1 + 1, 2);
});
運(yùn)行測試套件 & 使用測試 hooks 函數(shù)
describe 函數(shù)用于將相關(guān)測試分組到測試套件中。這使您的測試更有組織性并且更易于管理。
import { test, describe } from "node:test";
describe('My Test Suite', () => {
test('Test 1', () => {
// Test 1 logic
});
test('Test 2', () => {
// Test 2 logic
});
});
測試 hooks 函數(shù)是在測試之前或之后運(yùn)行的特殊函數(shù),它們對于設(shè)置或清理測試環(huán)境很有用。
test.beforeEach(() => {
// Runs before each test
});
test.afterEach(() => {
// Runs after each test
});
你還可以選擇使用 test.skip 函數(shù)跳過測試。這在某些你想暫時(shí)忽略特定測試時(shí)很有幫助。
test.skip('My skipped test', () => {
// Test logic
});
此外,Node.js 測試運(yùn)行器提供了不同的報(bào)告器(reporter),以各種方式格式化和顯示測試結(jié)果,使用 --reporter 選項(xiàng)指定。
node --test --test-reporter=tap
Jest 的缺陷
雖然 Jest 是 Node.js 社區(qū)中流行的測試框架,但它具有某些缺點(diǎn),讓原生 Node.js 測試運(yùn)行器成為更具吸引力的選擇。
Jest 即使作為一個(gè)開發(fā)依賴項(xiàng)安裝后,你將擁有一個(gè)包含不同許可證的 277 個(gè)其他依賴項(xiàng),這些許可證包括 MIT、Apache-2.0、CC-BY-4.0 和 1 個(gè)未知許可證。你知道嗎?
圖片
- Jest 會(huì)修改全局變量,這可能會(huì)導(dǎo)致測試中出現(xiàn)意外行為。
- instanceof 運(yùn)算符在 Jest 中并不總是按預(yù)期工作
- Jest 為你的項(xiàng)目引入了大量的依賴項(xiàng),你需要關(guān)心使用過程中的安全問題和開發(fā)時(shí)依賴項(xiàng)可能會(huì)引起的其他問題
- Jest 可能比原生 Node.js 測試運(yùn)行器要慢
原生 Node.js 測試運(yùn)行器的其他強(qiáng)大功能包括運(yùn)行子測試和并發(fā)測試。
子測試允許每個(gè) test() 回調(diào)接收一個(gè) context 參數(shù),這個(gè)參數(shù)允許你使用 context.test 方式創(chuàng)建嵌套測試。
如果你知道如何很好地使用并發(fā)測試并能避免競爭條件(racing condition),那么并發(fā)測試是一個(gè)很棒的功能。只需將 concurrency: true 作為第二個(gè)參數(shù)傳遞給 describe() 測試套件即可。
什么是測試運(yùn)行器?
測試運(yùn)行器是一種軟件工具,允許開發(fā)人員對其代碼進(jìn)行管理并執(zhí)行自動(dòng)化測試。 Node.js 測試運(yùn)行程序幫助你與 Node.js 可以無縫協(xié)作,為在 Node.js 應(yīng)用程序上編寫和運(yùn)行測試提供了一條龍服務(wù)。
Node.js 原生 mocking
Mocking(模擬)是開發(fā)人員用來隔離代碼進(jìn)行測試的一種策略,Node.js 運(yùn)行時(shí)引入了原生模擬功能,這對開發(fā)人員更加有效地理解和操作代碼幫助很多。
在此之前,你可能使用過其他測試框架的模擬功能,例如 Jest 的 jest.spyOn 或 mockResolvedValueOncel。當(dāng)你想要避免在測試中運(yùn)行實(shí)際代碼(例如 HTTP 請求或文件系統(tǒng) API)并期望稍后可以調(diào)用過程時(shí),模擬就非常有用了。
與其他 Node.js 運(yùn)行時(shí)功能(例如監(jiān)聽和測試覆蓋率功能)不同,模擬并未聲明為實(shí)驗(yàn)性的。但是,它后續(xù)可能會(huì)迎來更多更改,因?yàn)樗鼘?shí)在 Node.js 18 中才引入的新功能。
使用 node:test 中的 mock 進(jìn)行原生模擬測試
讓我們看看如何在實(shí)際示例中使用 Node.js 原生模擬功能。測試運(yùn)行器和(模塊)模擬功能現(xiàn)已作為穩(wěn)定功能在 Node.js 20 LTS 中提供了。
我們會(huì)用到一個(gè)工具模塊 dotenv.js,它的作用是從 .env 文件加載環(huán)境變量。下面,我們將使用一個(gè)測試文件 dotenv.test.js 來測試 dotenv.js 模塊。
dotenv.js 內(nèi)容如下:
import fs from "node:fs/promises";
export async function loadEnv(path = ".env") {
const rawDataEnv = await fs.readFile(path, "utf8");
const env = {};
rawDataEnv.split("\n").forEach((line) => {
const [key, value] = line.split("=");
env[key] = value;
});
return env;
}
在 dotenv.js 文件中,我們有一個(gè)異步函數(shù) loadEnv ,它使用 fs.readFile 方法讀取文件并將文件內(nèi)容拆分為鍵值對保存在 env 對象中并返回。
現(xiàn)在,讓我們看看如何使用 Node.js 中的原生模擬功能來測試這個(gè)函數(shù)。
// dotenv.test.js
import { describe, test, mock } from "node:test";
import assert from "node:assert";
import fs from "node:fs/promises";
import { loadEnv } from "../src/dotenv.js";
describe("dotenv test suite", () => {
test("should load env file", async () => {
const mockImplementation = async (path) => {
return "PORT=3000\n";
};
const mockedReadFile = mock.method(fs, "readFile", mockImplementation);
const env = await loadEnv(".env");
assert.strictEqual(env.PORT, "3000");
assert.strictEqual(mockedReadFile.mock.calls.length, 1);
});
});
在測試文件中,我們從 node:test 導(dǎo)入 mock 方法,用它來創(chuàng)建 fs.readFile 的模擬實(shí)現(xiàn)。在模擬實(shí)現(xiàn)中,無論傳遞的文件路徑如何,我們都會(huì)返回一個(gè)字符串 "PORT=3000\n"。
然后我們調(diào)用 loadEnv 函數(shù),并使用 assert 模塊,我們檢查 2 個(gè)地方:
- 返回的對象有一個(gè)值為 "3000" 的 PORT 屬性
- fs.readFile 方法只被調(diào)用了一次
通過使用 Node.js 中的原生模擬功能,我們能夠有效地將 loadEnv 函數(shù)與文件系統(tǒng)隔離并單獨(dú)測試。Node.js 20 的模擬功能還支持模擬計(jì)時(shí)器。
什么是 mocking?
在軟件測試中,模擬是用自定義實(shí)現(xiàn)替換特定模塊的實(shí)際功能的過程。主要目標(biāo)是將正在測試的代碼與外部依賴項(xiàng)隔離,確保測試僅驗(yàn)證單元的功能而不驗(yàn)證依賴項(xiàng)。模擬還能幫助你你模擬不同的測試場景,例如來自依賴項(xiàng)的錯(cuò)誤,這可能很難在真實(shí)環(huán)境驗(yàn)證。
Node.js 原生測試覆蓋率
什么是測試覆蓋率?
測試覆蓋率是軟件測試中使用的一個(gè)指標(biāo)。它可以幫助開發(fā)人員了解應(yīng)用程序源代碼的測試程度。這很重要,因?yàn)樗故玖舜a庫中未經(jīng)測試的區(qū)域,使開發(fā)人員能夠識(shí)別其軟件中的潛在弱點(diǎn)。
為什么測試覆蓋率很重要?因?yàn)樗峭ㄟ^減少 BUG 數(shù)量和避免回歸來確保軟件的質(zhì)量。此外,它還可以深入了解測試的有效性,并幫助指導(dǎo)我們構(gòu)建更強(qiáng)大、可靠和安全的應(yīng)用程序。
使用原生 Node.js 測試覆蓋率
從 v20 開始,Node.js 運(yùn)行時(shí)包含測試覆蓋率的功能。不過,原生 Node.js 測試覆蓋率目前被標(biāo)記為實(shí)驗(yàn)性功能,表示雖然現(xiàn)在可用,但在未來版本中可能會(huì)發(fā)生一些變化。
要使用原生 Node.js 測試覆蓋率,你需要使用 --experimental-coverage 命令行標(biāo)志。以下示例說明了如何在運(yùn)行項(xiàng)目測試的 package.json 腳本字段中添加 test:coverage 腳本:
{
"scripts": {
"test": "node --test ./tests",
"test:coverage": "node --experimental-coverage --test ./tests"
}
}
test:coverage 腳本中通過添加 --experimental-coverage 標(biāo)志就能在執(zhí)行測試期間生成覆蓋率數(shù)據(jù)了。
運(yùn)行 npm run test:coverage 后,你會(huì)看到類似于以下內(nèi)容的輸出:
? tests 7
? suites 4
? pass 5
? fail 0
? cancelled 0
? skipped 1
? todo 1
? duration_ms 84.018917
? start of coverage report
? ---------------------------------------------------------------------
? file | line % | branch % | funcs % | uncovered lines
? ---------------------------------------------------------------------
? src/dotenv.js | 100.00 | 100.00 | 100.00 |
? src/math.js | 100.00 | 100.00 | 100.00 |
? tests/dotenv.test.js | 100.00 | 100.00 | 100.00 |
? tests/math.test.js | 94.64 | 100.00 | 91.67 | 24-26
? ---------------------------------------------------------------------
? all files | 96.74 | 100.00 | 94.44 |
? ---------------------------------------------------------------------
? end of coverage report
這個(gè)報(bào)告展示了目前的測試所覆蓋的語句、分支、函數(shù)和行的百分占比。
Node.js 原生測試覆蓋率是一個(gè)強(qiáng)大的工具,可以幫助你提高 Node.js 應(yīng)用程序的質(zhì)量。盡管它目前被標(biāo)記為實(shí)驗(yàn)性功能,但它可以為你的測試覆蓋范圍提供有價(jià)值的見解并指導(dǎo)你的測試工作。通過了解和利用這個(gè)功能,可以確保你的代碼健壯、可靠且安全。
Node.js 監(jiān)聽模式
Node.js 監(jiān)聽模式是一項(xiàng)強(qiáng)大的開發(fā)人員功能,允許實(shí)時(shí)跟蹤 Node.js 文件的更改并自動(dòng)重新執(zhí)行腳本。
在深入了解 Node.js 的原生監(jiān)聽功能之前,有必要了解一下 nodemon[3],它是一個(gè)流行的工具程序,有助于滿足 Node.js 早期版本中的這一需求。Nodemon 是一個(gè)命令行界面 (CLI) 工具程序,用于在文件目錄中檢測到任何更改時(shí)重新啟動(dòng) Node.js 應(yīng)用程序。
npm install -g nodemon
nodemon
這個(gè)功能在開發(fā)過程中特別有用,避免每次修改文件時(shí)手動(dòng)重新啟動(dòng),可以節(jié)省時(shí)間并提高工作效率。
隨著 Node.js 本身的進(jìn)步,平臺(tái)本身提供了內(nèi)置功能來實(shí)現(xiàn)相同的結(jié)果,也不需要在項(xiàng)目中安裝額外的第三方依賴項(xiàng),例如 nodemon。
值得注意的是,Node.js 中的原生監(jiān)聽模式功能仍處于實(shí)驗(yàn)階段,未來可能會(huì)發(fā)生變化。請始終確保你使用的 Node.js 版本支持這個(gè)功能。
使用 Node.js 20 原生監(jiān)聽功能
Node.js 20 使用 --watch 命令行標(biāo)志引入了原生文件監(jiān)聽功能。這個(gè)功能使用起來很簡單,甚至支持匹配模式來滿足更復(fù)雜的文件監(jiān)聽需要。
node --watch app.js
同事,支持使用 glob 語法做匹配,當(dāng)你想要監(jiān)聽一組與特定模式匹配的文件時(shí),這特別有用:
node --watch 'lib/**/*.js' app.js
--watch 標(biāo)志還可以與 --test 結(jié)合使用,這樣修改測試文件時(shí)也能重新運(yùn)行測試:
node --watch --test '**/*.test.js'
需要注意的是,從 Node.js v20 開始,監(jiān)聽模式功能仍被標(biāo)記為實(shí)驗(yàn)性的,表示現(xiàn)在功能功能雖然齊全,但后續(xù)可能會(huì)有修改,不像其他非實(shí)驗(yàn)功能那么穩(wěn)定或經(jīng)過優(yōu)化。在實(shí)際s使用時(shí),可能會(huì)遇到一些 quirks 或 BUG。
Node.js Corepack
Node.js Corepack 是一個(gè)值得探索的有趣功能,它是在 Node.js 16 中引入的,至今仍被標(biāo)記為實(shí)驗(yàn)性的。
什么是 Corepack?
Corepack 是一個(gè)零運(yùn)行時(shí)依賴項(xiàng)目,充當(dāng) Node.js 項(xiàng)目和所要使用的包管理器之間的橋梁。安裝后,它提供了一個(gè)名為 corepack 的程序,開發(fā)人員可以在他們的項(xiàng)目中使用它,確保使用了正確的包管理器,而不必?fù)?dān)心其全局安裝。
為什么要使用 Corepack?
作為 JavaScript 開發(fā)人員,我們經(jīng)常處理多個(gè)項(xiàng)目,每個(gè)項(xiàng)目可能都有自己首選的包管理器。比如,一個(gè)項(xiàng)目使用 pnpm 管理其依賴項(xiàng),而另一個(gè)項(xiàng)目使用 yarn 管理其依賴項(xiàng),導(dǎo)致你需要在不同的包管理器之間切換。
這個(gè)可能就會(huì)導(dǎo)致沖突和不一致。Corepack 允許每個(gè)項(xiàng)目以無縫的方式指定和使用其首選的包管理器,從而解決了這個(gè)問題。
此外,Corepack 在你的項(xiàng)目和全局系統(tǒng)之間提供一層隔離,確保即使全局包升級或刪除,你的項(xiàng)目也能正常運(yùn)行,這提高了項(xiàng)目的一致性和可靠性。
安裝和使用 Corepack
安裝 Corepack 非常簡單。由于它從版本 16 開始與 Node.js 捆綁在一起,因此你只需安裝或升級 Node.js 到這個(gè)或更高版本即可。
安裝后,你可以在 package.json 文件中為項(xiàng)目指定包管理器,如下所示:
{
"packageManager": "yarn@2.4.1"
}
然后,你可以在項(xiàng)目中啟用 Corepack,如下所示:
corepack enable
如果你在項(xiàng)目目錄下的終端中輸入 yarn,即便當(dāng)前沒有安裝 Yarn,Corepack 會(huì)自動(dòng)檢測并安裝正確版本的。
這種做法會(huì)確保始終使用 Yarn v2.4.1 版本來安裝項(xiàng)目的依賴項(xiàng),無論系統(tǒng)上安裝的全局 Yarn 版本如何。
如果你想全局安裝 Yarn 或使用特定版本,您可以運(yùn)行:
corepack install --global yarn@stable
Corepack 仍然是一個(gè)實(shí)驗(yàn)性功能
盡管在 Node.js 16 中引入了 Corepack,但它仍然被標(biāo)記為實(shí)驗(yàn)性的。這表示雖然它現(xiàn)在設(shè)計(jì)并運(yùn)行的良好,但仍在積極開發(fā)中,并且其行為的某些方面將來可能會(huì)發(fā)生變化。
總之,Corepack 易于安裝、使用簡單,能為你的項(xiàng)目提供額外的可靠性,這絕對是一個(gè)值得探索并融入到你的開發(fā)工作流程中的功能。
Node.js .env 加載器
應(yīng)用程序的配置至關(guān)重要,作為 Node.js 開發(fā)人員,通常會(huì)面臨管理 API 憑據(jù)、服務(wù)器端口號或數(shù)據(jù)庫等配置的需求。
而我們需要一種方法,能在不改變源代碼的情況下,為不同的環(huán)境提供不同的設(shè)置。在 Node.js 應(yīng)用程序中實(shí)現(xiàn)這個(gè)目的的一種流行方法是使用存儲(chǔ)在 .env 文件中的環(huán)境變量。
npm 包 dotenv
在 Node.js 引入對加載 .env 文件的原生支持之前,開發(fā)人員主要使用 dotenv npm 包。dotenv 包將環(huán)境變量從 .env 文件加載到 process.env 中,然后在整個(gè)應(yīng)用程序中都可訪問了。
以下是 dotenv 包的典型用法:
require('dotenv').config();
console.log(process.env.MY_VARIABLE);
很有用,但需要向你的項(xiàng)目添加額外的依賴項(xiàng)。而在通過引入原生 .env 加載器后,你現(xiàn)在可以直接加載環(huán)境變量,而無需任何外部包。
使用原生 .env 加載器
從 Node.js 20 開始,內(nèi)置了從 .env 文件加載環(huán)境變量的功能。這個(gè)功能正在積極開發(fā)中,但對開發(fā)人員來說已經(jīng)游戲改變者了。
要加載 .env 文件,我們可以在啟動(dòng) Node.js 應(yīng)用程序時(shí)使用 --env-file CLI 標(biāo)志。該標(biāo)志指定要加載的 .env 文件的路徑。
node --env-file=./.env index.js
這會(huì)將環(huán)境變量從指定的 .env 文件加載到 process.env 中。然后,這些變量就可以像以前一樣在你的應(yīng)用程序中那樣使用了。
加載多個(gè) .env 文件
Node.js .env 加載器還支持加載多個(gè) .env 文件。當(dāng)你針對不同環(huán)境(例如開發(fā)、測試、生產(chǎn))有不同的環(huán)境變量集時(shí),這非常有用。
你可以指定多個(gè) --env-file 標(biāo)志來加載多個(gè)文件。文件按照指定的順序加載,后面文件中的變量會(huì)覆蓋前面文件中的變量。
node --env-file=./.env.default --env-file=./.env.development index.js
在此示例中, ./.env.default 包含默認(rèn)變量, ./.env.development 包含特定于開發(fā)環(huán)境的變量。 ./.env.development、./.env.default 中同時(shí)存在的變量,前者都將覆蓋后者中的變量。
Node.js 中對加載 .env 文件的原生支持對于 Node.js 開發(fā)人員來說是一項(xiàng)重大改進(jìn),它簡化了配置管理并消除了對額外軟件包的需要。
開始在 Node.js 應(yīng)用程序中使用 --env-file CLI 標(biāo)志并親身體驗(yàn)便利吧!
Node.js import.meta 支持 __dirname 和 __file
__dirname 和 __file 其實(shí)是 CommonJS 模塊中提供的變量,通常用來作為獲取當(dāng)前文件的目錄名稱和文件路徑的方式。然而,直到最近,這些在 ESM 上還不容易獲得,N你必須使用以下代碼來提取 __dirname:
import url from 'url'
import path from 'path'
const dirname = path.dirname(url.fileURLToPath(import.meta.url))
或者,如果你是 Matteo Collina 的粉絲,您可能已經(jīng)選擇使用 Matteo 的 desm[4] npm 包。
Node.js 不斷發(fā)展,為開發(fā)人員提供更有效的方法來處理文件和路徑操作。Node.js v20.11.0 和 Node.js v21.2.0 中引入了一項(xiàng)對 Node.js 開發(fā)人員的重大更改,即內(nèi)置了對 import.meta.dirname 和 import.meta.filename 的支持。
使用 Node.js import.meta.filename 和 import.meta.dirname
值得慶幸的是,隨著 import.meta.filename 和 import.meta.dirname 的引入,這個(gè)過程變得更加容易,讓我們看一下使用新功能加載配置文件的示例。
假設(shè)你需要加載的 JavaScript 文件所在目錄中有一個(gè) YAML 配置文件,你可以這樣做:
import fs from 'fs';
const { dirname: __dirname, filename: __filename } = import.meta;
const projectSetup = fs.readFileSync(`${__dirname}/setup.yml`, "utf8");
console.log(projectSetup);
在這個(gè)例子中,我們使用 import.meta.dirname 來獲取當(dāng)前文件的目錄名,并將其分配給 __dirname 變量,以便符合 CommonJS 的命名約定。
Node.js 原生計(jì)時(shí)器 Promise
我們都知道,Node.js 是一種基于 Chrome V8 JavaScript 引擎構(gòu)建的流行 JavaScript 運(yùn)行時(shí),始終致力于通過不斷的更新和新功能讓開發(fā)人員的生活變得更加輕松。
盡管 Node.js 在 Node.js v15 中就引入了原生計(jì)時(shí)器 Promise 的支持,但我承認(rèn)我并沒有經(jīng)常使用它們。
JavaScript 計(jì)時(shí)器的簡要回顧
在深入探討本機(jī)計(jì)時(shí)器 Promise 之前,我們來簡要回顧一下 JavaScript setTimeout() 和 setInterval() 計(jì)時(shí)器。
setTimeout() API 是一個(gè) JavaScript 函數(shù),一旦計(jì)時(shí)器到期,它就會(huì)執(zhí)行函數(shù)或指定的代碼段。
setTimeout(function(){
console.log("Hello World!");
}, 3000);
在上面的代碼中,“Hello World!”將在 3 秒(3000 毫秒)后打印到控制臺(tái)。
另一方面,setInterval() 則用于重復(fù)執(zhí)行指定的函數(shù),每次調(diào)用之間間隔指定延遲。
setInterval(function(){
console.log("Hello again!");
}, 2000);
在上面的代碼中,“Hello again!”每 2 秒(2000 毫秒)就會(huì)打印到控制臺(tái)。
舊方法:用 Promise 包裝 setTimeout()
在過去,開發(fā)人員通常必須人為地包裝 setTimeout() 函數(shù),并通過 Promise 使用它,這樣做是為了將 setTimeout() 和 async/await 一起使用。
下面是一個(gè)封裝的示例:
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function demo() {
console.log('Taking a break...');
await sleep(2000);
console.log('Two seconds later...');
}
demo();
這將打印"Taking a break...",等待兩秒鐘,然后打印 "Two seconds later..."。
這么做沒問題,卻給代碼增加了不必要的復(fù)雜性。
Node.js Native 計(jì)時(shí)器 Promise
使用 Node.js Native 計(jì)時(shí)器 Promise,我們不再需要將 setTimeout() 包裝在 Promise 中。相反,我們可以直接將 setTimeout() 與 async/await 一起使用。
以下是如何使用 Node.js 原生計(jì)時(shí)器 Promise 的示例:
const {
setTimeout,
} = require('node:timers/promises');
setTimeout(2000, 'Two seconds later...').then((res) => {
console.log(res);
});
console.log('Taking a break...');
在上面的代碼中,setTimeout() 是從 node:timers/promises 導(dǎo)入的。然后我們直接將其與 async/await 一起使用。它將打印"Taking a break...",等待兩秒鐘,然后打印 "Two seconds later..."。
這極大地簡化了異步編程,并使代碼更易于閱讀、編寫和維護(hù)。
Node.js 權(quán)限模型
目前加入 Node.js TSC 的 Rafael Gonzaga 恢復(fù)了 Node.js 權(quán)限模塊的開發(fā)工作,這個(gè)模塊與 Deno 類似,提供了一組進(jìn)程級的可配置資源約束。
出于安全和合規(guī)性原因,管理和控制 Node.js 應(yīng)用程序可以訪問的資源變得越來越重要。因?yàn)橛袝r(shí)候你也不知道項(xiàng)目使用的 npm 包突然被黑了,或是誤下載了一個(gè)惡意 npm 包等情況的發(fā)生。
在這方面,Node.js 引入了一項(xiàng)稱為權(quán)限模塊的實(shí)驗(yàn)性功能,用于管理 Node.js 應(yīng)用程序中的資源權(quán)限。使用 --experimental-permission 命令行標(biāo)志可以啟用這個(gè)功能。
Node.js 資源權(quán)限模型
Node.js 中的權(quán)限模型提供了一套抽象來管理對各種資源(如文件系統(tǒng)、網(wǎng)絡(luò)、環(huán)境變量和工作線程等)的訪問。當(dāng)你想要限制應(yīng)用程序的某個(gè)部分可以訪問的資源時(shí),這個(gè)功能就很有用。
你可以使用權(quán)限模型的常見資源約束,包括:
- 使用 --allow-fs-read=* 和 --allow-fs-write=* 進(jìn)行文件系統(tǒng)讀寫,可以指定目錄和特定文件路徑,還可以通過重復(fù)標(biāo)志來限定多種資源
- 使用 --allow-child-process 進(jìn)行子進(jìn)程調(diào)用
- 使用 --allow-worker 進(jìn)行工作線程調(diào)用
Node.js 權(quán)限模型還通過 process.permission.has(resource, value) 提供運(yùn)行時(shí) API,以允許查詢特定訪問權(quán)限。
如果你嘗試訪問不能允許的資源,例如讀取 .env 文件,就會(huì)看到 ERR_ACCESS_DENIED 錯(cuò)誤:
> start:protected
> node --env-file=.env --experimental-permission server.js
node:internal/modules/cjs/loader:197
const result = internalModuleStat(filename);
^
Error: Access to this API has been restricted
at stat (node:internal/modules/cjs/loader:197:18)
at Module._findPath (node:internal/modules/cjs/loader:682:16)
at resolveMainPath (node:internal/modules/run_main:28:23)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:135:24)
at node:internal/main/run_main_module:28:49 {
code: 'ERR_ACCESS_DENIED',
permission: 'FileSystemRead',
resource: '/Users/lirantal/repos/modern-nodejs-runtime-features-2024/server.js'
}
Node.js v20.14.0
Node.js 權(quán)限模型示例
考慮這樣一個(gè)場,有一個(gè)處理文件上傳的 Node.js 應(yīng)用程序。你希望限制應(yīng)用程序的這一部分,以便它只能訪問存儲(chǔ)上傳文件的特定目錄。
使用 --experimental-permission 標(biāo)志啟動(dòng) Node.js 應(yīng)用程序時(shí)啟用實(shí)驗(yàn)性權(quán)限功能。
node --experimental-permission ./app.js
我們還想專門允許應(yīng)用程序讀取 2 個(gè)受信任的文件 .env 和 setup.yml ,因此我們需要將上面的內(nèi)容更新為:
node --experimental-permission --allow-fs-write=/tmp/uploads --allow-fs-read=.env --allow-fs-read=setup.yml ./app.js
這樣一來,如果應(yīng)用程序?qū)μ峁┑纳蟼髀窂街獾奈募M(jìn)行寫入,就會(huì)因報(bào)錯(cuò)而終止。
請參閱以下代碼示例,了解如何通過 try/catch 包裝資源訪問的地方,或通過權(quán)限檢 API 來進(jìn)行防御性編程,確保不會(huì)因?yàn)闄?quán)限模型的引入導(dǎo)致出現(xiàn)的異常意外終止程序。
const { dirname: __dirname, filename: __filename } = import.meta;
// @TODO to avoid the Node.js resource permission issue you should update
// the path to be `setup.yml` in the current directory and not `../setup.yml`.
// the outside path for setup.yml was only changed in the source code to
// show you how Node.js resource permission module will halt if trying to access
// something outside the current directory.
const filePath = `${__dirname}/../setup.yml`;
try {
const projectSetup = fs.readFileSync(filePath, "utf8");
// @TODO do something with projectSetup if you want to
} catch (error) {
console.error(error.code);
}
// @TODO or consider using the permissions runtime API check:
if (!process.permission.has("read", filePath)) {
console.error("no permissions to read file at", filePath);
}
值得注意的是,Node.js 中的權(quán)限功能仍處于實(shí)驗(yàn)階段,未來可能會(huì)發(fā)生變化。
Node.js 策略模塊
Node.js 策略模塊是一項(xiàng)安全功能,旨在防止惡意代碼在 Node.js 應(yīng)用程序中加載和執(zhí)行。雖然它不會(huì)追蹤加載代碼的來源,但它提供了針對潛在威脅的可靠防御機(jī)制。
策略模塊利用 --experimental-policy CLI 標(biāo)志來啟用基于策略的代碼加載。此標(biāo)志采用策略清單文件(JSON 格式)作為參數(shù)。例如, --experimental-policy=policy.json。
策略清單文件包含 Node.js 在加載模塊時(shí)遵循的策略,這提供了一種強(qiáng)大的方法來控制加載到應(yīng)用程序中的代碼的性質(zhì)。
使用 Node.js 策略模塊
讓我們通過一個(gè)簡單的示例來演示如何使用 Node.js 策略模塊:
- 創(chuàng)建策略文件。這個(gè)文件應(yīng)該是一個(gè) JSON 文件,指定應(yīng)用程序加載模塊的策略。我們稱之為 policy.json。
類似:
{
"resources": {
"./moduleA.js": {
"integrity": "sha384-xxxxx"
},
"./moduleB.js": {
"integrity": "sha384-yyyyy"
}
}
}
這個(gè)策略文件指定 moduleA.js 和 moduleB.js 應(yīng)加載特定的 integrity 值。
然而,為所有直接和間接依賴項(xiàng)生成策略文件并不簡單。幾年前,Bradley Meck 創(chuàng)建了 node-policy npm 包,它提供了一個(gè) CLI 來自動(dòng)生成策略文件。
- 使用 --experimental-policy 標(biāo)志運(yùn)行 Node.js 應(yīng)用程序
node --experimental-policy=policy.json app.js
這個(gè)命令告訴 Node.js 在加載 app.js 中的模塊時(shí)遵守 policy.json 中指定的策略。
- 為了防止篡改策略文件,你可以使用 --policy-integrity 標(biāo)志為策略文件本身提供 integrity 值:
node --experimental-policy=policy.json --policy-integrity="sha384-zzzzz" app.js
這個(gè)命令可確保保持策略文件的完整性,即使磁盤上的文件發(fā)生更改也是如此。
integrity 策略的注意事項(xiàng)
Node.js 運(yùn)行時(shí)沒有內(nèi)置功能來生成或管理策略文件,并且可能會(huì)帶來困難,例如根據(jù)生產(chǎn)環(huán)境與開發(fā)環(huán)境以及動(dòng)態(tài)模塊導(dǎo)入來管理不同的策略。
另一個(gè)需要注意的是,如果你已經(jīng)擁有處于當(dāng)前狀態(tài)的惡意 npm 包,那么生成模塊完整性策略文件就為時(shí)已晚。
我個(gè)人建議你可以持續(xù)關(guān)注這方面的更新,并慢慢嘗試逐步采用此功能。
有關(guān) Node.js 策略模塊的更多信息,你可以查看有關(guān)向 Node.js 引入實(shí)驗(yàn)性完整性策略[5]這篇文章,其中提供了有關(guān)使用 Node.js 策略完整性的更詳細(xì)的詳細(xì)教程。
總結(jié)
當(dāng)我們?yōu)g覽了你應(yīng)該在 2024 年開始使用的現(xiàn)代 Node.js 運(yùn)行時(shí)功能時(shí),很明顯,這些功能幫助你簡化開發(fā)流程、增強(qiáng)應(yīng)用程序性能并加強(qiáng)安全性。這些功能不僅僅是時(shí)尚,也重新定義了 Node.js 開發(fā)方式的巨大潛力。
參考資料
[1]10 modern Node.js runtime features to start using in 2024: https://snyk.io/blog/10-modern-node-js-runtime-features/
[2]corepack: https://nodejs.org/api/corepack.html
[3]nodemon: https://snyk.io/advisor/npm-package/nodemon
[4]desm: https://snyk.io/advisor/npm-package/desm
[5]向 Node.js 引入實(shí)驗(yàn)性完整性策略: https://snyk.io/blog/introducing-experimental-integrity-policies-to-node-js/
原文鏈接:10 modern Node.js runtime features to start using in 2024[1], 2024.5.29, by Liran Tal。翻譯時(shí)有刪改。