Skip to main content

10 moderne Node.js-Runtime-Funktionen, die Sie 2024 nutzen sollten

Artikel von
feature java dto

29. Mai 2024

0 Min. Lesezeit
10 Node.js runtime features you SHOULD be using in 2024

Die serverseitige JavaScript-Runtime entwickelt sich rasant weiter: Bun macht Fortschritte mit kompatiblen Node.js-APIs, während die Node.js-Runtime eine umfangreiche Standardbibliothek und vielseitige Runtime-Funktionen bietet.

Zu Beginn des Jahres 2024 bietet dieser Artikel eine gute Gelegenheit, sich über die neuesten Funktionen und Möglichkeiten der Node.js-Runtime auf dem Laufenden zu halten. Auf dem neuesten Stand zu bleiben bedeutet nicht nur, „mit der Zeit zu gehen“ – es geht darum, moderne APIs zu nutzen, um effizienteren, leistungsfähigeren und sichereren Code zu schreiben.

In diesem Beitrag stellen wir 10 moderne Node.js-Runtime-Funktionen vor, die alle Entwicklerinnen und Entwickler 2024 nutzen sollten. Wir behandeln alles – von brandneuen APIs bis hin zu den spannenden Funktionen der Newcomer Bun und Deno.

Voraussetzung: Node.js-LTS-Version

Bevor Sie diese modernen Funktionen erkunden, sollten Sie sicherstellen, dass Sie die Node.js-LTS-Version (Long-Term Support) verwenden. Zum Zeitpunkt der Erstellung dieses Artikels ist v20.14.0 die neueste Node.js-LTS-Version.

Mit folgendem Befehl können Sie Ihre Node.js-Version überprüfen:

node --version

Wenn Sie derzeit nicht die LTS-Version verwenden, können Sie mit einem Versionsmanager wie fnm oder nvm ganz einfach zwischen verschiedenen Node.js-Versionen wechseln.

Was ist neu in Node.js 20?

In den folgenden Abschnitten stellen wir einige neue Funktionen vor, die in den letzten Node.js-Versionen hinzugekommen sind. Einige sind stabil, andere noch experimentell. Manche werden schon länger unterstützt, sind Ihnen aber möglicherweise noch nicht bekannt.

Wir beschäftigen uns mit folgenden Themen:

  1. Node.js-Test-Runner

  2. Native Node.js-Mock-Funktionen

  3. Native Node.js-Testabdeckung

  4. Node.js-Watch-Modus

  5. Node.js-Corepack

  6. Node.js-.env-Loader

  7. Node.js import.meta.file für __dirname und __file

  8. Native Node.js-Timer-Promises

  9. Node.js-Berechtigungsmodul

  10. Node.js-Policy-Modul

1. Der native Node.js-Test-Runner

Welche Möglichkeiten gab es, bevor Node.js einen Test-Runner in die native Runtime integrierte? Bislang haben Sie wahrscheinlich eine der beliebten Lösungen wie node-tap, jest, mocha oder vitest verwendet.

Sehen wir uns an, wie Sie den nativen Node.js-Test-Runner in Ihren Entwicklungsworkflow integrieren. Importieren Sie zunächst das Testmodul aus Node.js in Ihre Testdatei, wie unten gezeigt:

import { test } from 'node:test';

Gehen wir nun die einzelnen Schritte zur Verwendung des Node.js-Test-Runners durch.

Einen einzelnen Test mit node:test ausführen

Um einen einzelnen Test zu erstellen, verwenden Sie die Funktion test und übergeben ihr den Namen des Tests sowie eine Callback-Funktion. In dieser Callback-Funktion definieren Sie die Testlogik.

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");
  });
});

Führen Sie den Test mit dem Befehl node --test aus, gefolgt vom Namen Ihrer Testdatei:

node --test tests/math.test.js

Der Node.js-Test-Runner kann Testdateien in Ihrem Projekt automatisch erkennen und ausführen. Üblicherweise enden diese Dateien auf .test.js, diese Namenskonvention ist jedoch nicht zwingend.

Wenn Sie das Positionsargument für die Testdatei weglassen, verwendet der Node.js-Test-Runner Heuristiken und Glob-Muster, um Testdateien zu finden. Dazu gehören beispielsweise alle Dateien im Ordner test/ oder tests/ sowie Dateien mit dem Präfix test- oder der Endung .test.

Beispiel für den Abgleich von Testdateien mit Glob-Mustern:

node --test '**/*.test.js'

Test-Assertions mit node:assert verwenden

Der Node.js-Test-Runner unterstützt Assertions über das integrierte Modul assert. Mit verschiedenen Methoden wie assert.strictEqual können Sie Ihre Tests überprüfen.

import assert from 'node:assert';

test('Test 1', () => {
  assert.strictEqual(1 + 1, 2);
});

Testsuiten und Test-Hooks mit dem nativen Node.js-Test-Runner

Mit der Funktion describe können Sie zusammengehörige Tests zu einer Testsuite gruppieren. Dadurch lassen sich Ihre Tests übersichtlicher organisieren und leichter verwalten.

import { test, describe } from "node:test";

describe('My Test Suite', () => {
  test('Test 1', () => {
    // Test 1 logic
  });

  test('Test 2', () => {
    // Test 2 logic
  });
});

Test-Hooks sind spezielle Funktionen, die vor oder nach Ihren Tests ausgeführt werden. Sie eignen sich zum Einrichten oder Bereinigen von Testumgebungen.

test.beforeEach(() => {
  // Runs before each test
});

test.afterEach(() => {
  // Runs after each test
});

Mit der Funktion test.skip können Sie einen Test auch überspringen. Das ist hilfreich, wenn Sie einen bestimmten Test vorübergehend auslassen möchten.

test.skip('My skipped test', () => {
  // Test logic
});

Außerdem bietet der Node.js-Test-Runner verschiedene Reporter, die Testergebnisse auf unterschiedliche Weise formatieren und anzeigen. Mit der Option --reporter können Sie einen Reporter festlegen.

node --test --test-reporter=tap

Sollten Sie Jest den Rücken kehren?

Jest ist zwar ein beliebtes Test-Framework in der Node.js-Community, hat jedoch einige Nachteile, durch die der native Node.js-Test-Runner attraktiver sein kann.

Wenn Sie Jest installieren, fügen Sie Ihrem Projekt selbst als reine Entwicklungsabhängigkeit 277 transitive Abhängigkeiten mit verschiedenen Lizenzen hinzu, darunter MIT, Apache-2.0, CC-BY-4.0 und eine unbekannte Lizenz. Wussten Sie das?

Transitives Abhängigkeitsdiagramm von Jest.
  • Jest verändert globale Objekte, was zu unerwartetem Verhalten in Ihren Tests führen kann.

  • Der Operator instanceof funktioniert in Jest nicht immer wie erwartet.

  • Jest bringt viele Abhängigkeiten in Ihr Projekt ein. Dadurch wird es schwieriger, Drittanbieter-Abhängigkeiten auf dem neuesten Stand zu halten. Außerdem müssen Sie sich unnötig um Sicherheitsprobleme und andere Aspekte von Entwicklungsabhängigkeiten kümmern.

  • Aufgrund des zusätzlichen Aufwands kann Jest langsamer sein als der native Node.js-Test-Runner.

Zu den weiteren nützlichen Funktionen des nativen Node.js-Test-Runners gehören Untertests und nebenläufige Tests. Bei Untertests erhält jeder test()-Callback ein context-Argument, mit dem Sie über context.test verschachtelte Tests erstellen können. Nebenläufige Tests sind eine gute Funktion, wenn Sie wissen, wie Sie sie richtig einsetzen und Race Conditions vermeiden. Übergeben Sie dem Test-Suite-Aufruf describe() einfach ein Objekt mit der möglichen Eigenschaft concurrency: true als zweites Argument.

Was ist ein Test-Runner?

Ein Test-Runner ist ein Softwaretool, mit dem Entwickler automatisierte Tests für ihren Code verwalten und ausführen können. Der Node.js-Test-Runner ist ein Framework, das nahtlos mit Node.js zusammenarbeitet und eine umfangreiche Umgebung zum Schreiben und Ausführen von Tests für Ihre Node.js-Anwendungen bietet.

2. Native Node.js-Mock-Funktionen

Mocking ist eine Strategie, mit der Entwicklerinnen und Entwickler Code für Tests isolieren. Die Node.js-Runtime bietet inzwischen native Mock-Funktionen, die Sie kennen und effektiv einsetzen sollten.

Wahrscheinlich haben Sie schon Mock-Funktionen anderer Test-Frameworks verwendet, etwa Jest-Funktionen wie jest.spyOn oder mockResolvedValueOncel. Sie sind nützlich, wenn Sie in Ihren Tests keinen echten Code ausführen möchten, etwa HTTP-Anfragen oder Dateisystem-APIs. Stattdessen können Sie diese Vorgänge durch Stubs und Mocks ersetzen, die Sie später überprüfen können.

Anders als andere Node.js-Runtime-Funktionen wie Watch und Coverage ist Mocking nicht als experimentell gekennzeichnet. Da es erst mit Node.js 18 eingeführt wurde, ist jedoch mit weiteren Änderungen zu rechnen.

Native Node.js-Mocks mit import { mock } from 'node:test'

Sehen wir uns anhand eines praktischen Beispiels an, wie Sie die native Mock-Funktion von Node.js verwenden können. Der Test-Runner und das Modul-Mocking sind in Node.js 20 LTS jetzt als stabile Funktionen verfügbar.

Wir verwenden das Utility-Modul dotenv.js, das Umgebungsvariablen aus einer Datei .env lädt. Außerdem nutzen wir die Testdatei dotenv.test.js, mit der wir das Modul dotenv.js testen.

Hier ist unser eigenes internes dotenv-Modul:

// dotenv.js
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;
}

In der Datei dotenv.js befindet sich die asynchrone Funktion loadEnv. Sie liest mithilfe der Methode fs.readFile eine Datei ein und zerlegt deren Inhalt in Schlüssel-Wert-Paare. Wie Sie sehen, verwendet sie die native Node.js-Dateisystem-API fs.

Sehen wir uns nun an, wie Sie diese Funktion mit der nativen Mock-Funktion in Node.js testen können.

// 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);
  });
});

In der Testdatei importieren wir die Methode mock aus node:test. Damit erstellen wir eine Mock-Implementierung von fs.readFile. Diese gibt unabhängig vom übergebenen Dateipfad den String "PORT=3000\n" zurück.

Anschließend rufen wir die Funktion loadEnv auf und überprüfen mithilfe des Moduls assert zwei Dinge:

  1. Das zurückgegebene Objekt hat eine Eigenschaft PORT mit dem Wert "3000".

  2. Die Methode fs.readFile wurde genau einmal aufgerufen.

Mit der nativen Mock-Funktion von Node.js können wir unsere Funktion loadEnv effektiv vom Dateisystem isolieren und unabhängig testen. Die Mock-Funktionen in Node.js 20 unterstützen auch Timer.

Was bedeutet Mocking?

Beim Softwaretesten werden beim Mocking die tatsächlichen Funktionen bestimmter Module durch künstliche ersetzt. Das Hauptziel ist, die getestete Codeeinheit von externen Abhängigkeiten zu isolieren. So wird sichergestellt, dass der Test nur die Funktionalität der Einheit und nicht die der Abhängigkeiten überprüft. Mit Mocking können Sie außerdem verschiedene Szenarien simulieren, etwa Fehler in Abhängigkeiten, die sich in einer realen Umgebung nur schwer zuverlässig reproduzieren lassen.

3. Native Node.js-Testabdeckung

Was ist Testabdeckung?

Die Testabdeckung ist eine Kennzahl in der Softwareprüfung. Sie hilft Entwicklern zu verstehen, in welchem Umfang der Quellcode einer Anwendung getestet wird. Das ist entscheidend, denn sie macht Bereiche der Codebasis sichtbar, die noch nicht getestet wurden. So können Entwickler potenzielle Schwachstellen in ihrer Software erkennen.

Warum ist Testabdeckung wichtig? Sie trägt zur Softwarequalität bei, indem sie die Zahl der Fehler reduziert und Regressionen verhindert. Außerdem erhalten Sie Einblicke in die Wirksamkeit Ihrer Tests und können damit auf robustere, zuverlässigere und sicherere Anwendungen hinarbeiten.

Native Node.js-Testabdeckung nutzen

Ab Version 20 bietet die Node.js-Runtime native Funktionen zur Testabdeckung. Beachten Sie jedoch, dass die native Node.js-Testabdeckung derzeit als experimentell gekennzeichnet ist. Sie können sie zwar verwenden, in künftigen Releases sind aber möglicherweise Änderungen zu erwarten.

Für die native Node.js-Testabdeckung müssen Sie das Befehlszeilen-Flag --experimental-test-coverage verwenden. So fügen Sie beispielsweise in das Skriptfeld Ihrer package.json einen Eintrag test:coverage ein, der die Tests Ihres Projekts ausführt:

{
  "scripts": {
    "test": "node --test ./tests",
    "test:coverage": "node --experimental-test-coverage --test ./tests"
  }
}

Im obigen Beispiel nutzt das Skript test:coverage das Flag --experimental-test-coverage, um während der Testausführung Abdeckungsdaten zu erstellen.

Nach der Ausführung von npm run test:coverage sollte eine ähnliche Ausgabe erscheinen:

ℹ 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

Dieser Bericht zeigt den prozentualen Anteil der Anweisungen, Verzweigungen, Funktionen und Zeilen, die von den Tests abgedeckt werden.

Die native Node.js-Testabdeckung ist ein leistungsstarkes Tool, mit dem Sie die Qualität Ihrer Node.js-Anwendungen verbessern können. Obwohl sie derzeit als experimentell gekennzeichnet ist, liefert sie wertvolle Einblicke in Ihre Testabdeckung und hilft Ihnen, Ihre Testaktivitäten gezielt auszurichten. Wenn Sie diese Funktion verstehen und nutzen, können Sie sicherstellen, dass Ihr Code robust, zuverlässig und sicher ist.

4. Node.js-Watch-Modus

Der Node.js-Watch-Modus ist eine leistungsstarke Entwicklerfunktion. Sie verfolgt Änderungen an Ihren Node.js-Dateien in Echtzeit und führt Skripte automatisch erneut aus.

Bevor wir uns die nativen Watch-Funktionen von Node.js ansehen, sollten wir nodemon erwähnen. Dieses beliebte Utility schloss in früheren Node.js-Versionen eine wichtige Lücke. Nodemon ist ein Befehlszeilen-Tool (CLI), das die Node.js-Anwendung neu startet, sobald eine Änderung im Dateiverzeichnis erkannt wird.

npm install -g nodemon
nodemon

Diese Funktion ist besonders während der Entwicklung nützlich. Sie spart Zeit und steigert die Produktivität, da die Anwendung nach jeder Dateiänderung nicht mehr manuell neu gestartet werden muss.

Snyk Advisor zeigt den Paketstatus des npm-Pakets nodemon.

Mit den Weiterentwicklungen von Node.js bietet die Sprache nun eine integrierte Funktion, die dasselbe leistet. Dadurch müssen Sie Ihrem Projekt keine zusätzlichen Drittanbieter-Abhängigkeiten wie nodemon hinzufügen.

Bevor wir mit dem Tutorial beginnen, sollten Sie beachten, dass der native Watch-Modus in Node.js noch experimentell ist und sich ändern kann. Stellen Sie sicher, dass Sie eine Node.js-Version verwenden, die diese Funktion unterstützt.

Native Watch-Funktionen in Node.js 20 verwenden

Node.js 20 bietet mit dem Befehlszeilen-Flag --watch native Funktionen zur Dateiüberwachung. Die Funktion ist einfach zu verwenden und unterstützt sogar Glob-Muster für komplexere Anforderungen an die Dateiüberwachung.

Um den Befehl --watch zu verwenden, fügen Sie ihn wie unten gezeigt in der Befehlszeile zu Ihrem Node.js-Skript hinzu:

node --watch app.js

Bei Glob-Mustern können Sie das Flag --watch mit einem bestimmten Muster verwenden, um mehrere Dateien oder Verzeichnisse zu überwachen. Das ist besonders nützlich, wenn Sie eine Gruppe von Dateien beobachten möchten, die einem bestimmten Muster entsprechen:

node --watch 'lib/**/*.js' app.js

Sie können das Flag --watch auch zusammen mit --test verwenden, damit Tests bei Änderungen an Testdateien erneut ausgeführt werden:

node --watch --test '**/*.test.js'

Diese Kombination kann Ihre testgetriebene Entwicklung (TDD) erheblich beschleunigen, da Ihre Tests nach jeder Änderung automatisch ausgeführt werden.

Beachten Sie, dass der Watch-Modus in Node.js 20 noch als experimentell gekennzeichnet ist. Die Funktion ist zwar voll funktionsfähig, möglicherweise aber nicht so stabil oder optimiert wie andere, nicht experimentelle Funktionen.

In der Praxis können bei Verwendung des Flags --watch Eigenheiten oder Fehler auftreten.

5. Node.js Corepack

Node.js Corepack ist eine interessante Funktion, die es sich lohnt, genauer anzusehen. Sie wurde in Node.js 16 eingeführt und ist weiterhin als experimentell gekennzeichnet. Umso spannender ist es, herauszufinden, was sie bietet und wie Sie sie in Ihren JavaScript-Projekten nutzen können.

Was ist Corepack?

Corepack ist ein Projekt ohne Laufzeitabhängigkeiten, das als Brücke zwischen Node.js-Projekten und den dafür vorgesehenen Paketmanagern dient. Nach der Installation stellt es ein Programm namens corepack bereit, mit dem Entwicklerinnen und Entwickler in ihren Projekten sicherstellen können, dass sie den passenden Paketmanager verwenden – ohne sich um dessen globale Installation kümmern zu müssen.

Warum Corepack verwenden?

Als JavaScript-Entwicklerinnen und -Entwickler arbeiten wir oft mit mehreren Projekten, von denen jedes seinen eigenen bevorzugten Paketmanager haben kann. Sie kennen das sicher: Ein Projekt verwaltet seine Abhängigkeiten mit pnpm, ein anderes mit yarn – und schon müssen Sie zwischen verschiedenen Versionen von Paketmanagern wechseln.

Das kann zu Konflikten und Inkonsistenzen führen. Corepack löst dieses Problem, indem es jedem Projekt ermöglicht, den bevorzugten Paketmanager nahtlos festzulegen und zu verwenden.

Darüber hinaus sorgt Corepack für eine Trennung zwischen Ihrem Projekt und dem globalen System. So bleibt Ihr Projekt ausführbar, selbst wenn globale Pakete aktualisiert oder entfernt werden. Das erhöht die Konsistenz und Zuverlässigkeit Ihres Projekts.

Corepack installieren und verwenden

Die Installation von Corepack ist unkompliziert. Da es ab Version 16 mit Node.js gebündelt ist, müssen Sie Node.js lediglich auf diese oder eine neuere Version aktualisieren oder installieren.

Nach der Installation können Sie den Paketmanager für Ihr Projekt in der Datei package.json festlegen, zum Beispiel so:

{
  "packageManager": "yarn@2.4.1"
}

Anschließend können Sie Corepack in Ihrem Projekt wie folgt verwenden:

corepack enable

Wenn Sie im Projektverzeichnis yarn eingeben und Yarn nicht installiert ist, erkennt Corepack automatisch die passende Version und installiert sie für Sie.

So wird sichergestellt, dass Yarn Version 2.4.1 zum Installieren der Abhängigkeiten Ihres Projekts verwendet wird – unabhängig davon, welche Yarn-Version global auf dem System installiert ist.

Wenn Sie Yarn global installieren oder eine bestimmte Version verwenden möchten, führen Sie Folgendes aus:

corepack install --global yarn@stable

Corepack: weiterhin eine experimentelle Funktion

Obwohl Corepack mit Node.js 16 eingeführt wurde, ist es weiterhin als experimentell gekennzeichnet. Das bedeutet: Die Funktion sollte zwar zuverlässig arbeiten, wird aber noch aktiv weiterentwickelt. Einzelne Aspekte ihres Verhaltens können sich daher künftig ändern.

Dennoch ist Corepack einfach zu installieren und zu verwenden und sorgt für zusätzliche Zuverlässigkeit in Ihren Projekten. Es lohnt sich auf jeden Fall, die Funktion auszuprobieren und in Ihren Entwicklungsworkflow zu integrieren.

6. Node.js .env-Loader

Eine gute Anwendungskonfiguration ist entscheidend. Als Node.js-Entwickler wissen Sie sicher, wie wichtig es ist, API-Zugangsdaten, Server-Portnummern oder Datenbankkonfigurationen zu verwalten.

Als Entwicklerinnen und Entwickler benötigen wir eine Möglichkeit, für verschiedene Umgebungen unterschiedliche Einstellungen bereitzustellen, ohne den Quellcode zu ändern. Eine beliebte Methode dafür in Node.js-Anwendungen ist die Verwendung von Umgebungsvariablen, die in .env-Dateien gespeichert sind.

Das npm-Paket dotenv

Bevor Node.js native Unterstützung zum Laden von .env-Dateien einführte, verwendeten Entwicklerinnen und Entwickler dafür meist das npm-Paket dotenv. Das Paket dotenv lädt Umgebungsvariablen aus einer .env-Datei in process.env, sodass sie in der gesamten Anwendung verfügbar sind.

So wird das Paket dotenv üblicherweise verwendet:

require('dotenv').config();

console.log(process.env.MY_VARIABLE);

Das funktionierte gut, erforderte jedoch eine zusätzliche Abhängigkeit in Ihrem Projekt. Mit dem nativen .env-Loader können Sie Ihre Umgebungsvariablen jetzt direkt laden, ohne externe Pakete zu benötigen.

Native Unterstützung zum Laden von .env-Dateien in Node.js

Ab Node.js 20 enthält die Laufzeitumgebung eine integrierte Funktion zum Laden von Umgebungsvariablen aus .env-Dateien. Die Funktion wird noch aktiv weiterentwickelt, hat sich für Entwicklerinnen und Entwickler aber bereits als echter Durchbruch erwiesen.

Zum Laden einer .env-Datei können Sie beim Starten Ihrer Node.js-Anwendung das CLI-Flag --env-file verwenden. Damit geben Sie den Pfad zur zu ladenden .env-Datei an.

node --env-file=./.env index.js

Dadurch werden die Umgebungsvariablen aus der angegebenen .env-Datei in process.env geladen. Anschließend sind die Variablen wie bisher in Ihrer Anwendung verfügbar.

Mehrere .env-Dateien laden

Der .env-Loader von Node.js unterstützt auch das Laden mehrerer .env-Dateien. Das ist nützlich, wenn Sie für verschiedene Umgebungen unterschiedliche Gruppen von Umgebungsvariablen verwenden, etwa für Entwicklung, Tests und Produktion.

Sie können mehrere --env-file-Flags angeben, um mehrere Dateien zu laden. Die Dateien werden in der angegebenen Reihenfolge geladen. Variablen aus späteren Dateien überschreiben dabei Variablen aus früheren Dateien.

Hier ein Beispiel:

node --env-file=./.env.default --env-file=./.env.development index.js

In diesem Beispiel enthält ./.env.default die Standardvariablen und ./.env.development die entwicklungsbezogenen Variablen. Variablen in ./.env.development, die auch in ./.env.default vorhanden sind, überschreiben die entsprechenden Werte in ./.env.default.

Die native Unterstützung von Node.js zum Laden von .env-Dateien ist eine deutliche Verbesserung für Node.js-Entwicklerinnen und -Entwickler. Sie vereinfacht die Konfigurationsverwaltung und macht ein zusätzliches Paket überflüssig. Verwenden Sie das CLI-Flag --env-file in Ihren Node.js-Anwendungen und überzeugen Sie sich selbst von den Vorteilen.

7. Node.js: Unterstützung für __dirname und __file mit import.meta

Wenn Sie von den CommonJS-Modulkonventionen in Node.js kommen, sind Sie es gewohnt, mit filename und __dirname den Verzeichnisnamen und den Dateipfad der aktuellen Datei abzurufen. Bis vor Kurzem waren diese Werte in ESM jedoch nicht ohne Weiteres verfügbar. Um __dirname zu ermitteln, mussten Sie folgenden Code verwenden:

import url from 'url'
import path from 'path'
const dirname = path.dirname(url.fileURLToPath(import.meta.url))

Oder wenn Sie ein Fan von Matteo Collina sind, haben Sie vielleicht sein npm-Paket desm verwendet.

Matteo Collinas desm-npm-Paket, das über Daten des import.meta-Objekts __dirname und __filename in ESM-Projekten bereitstellt.

Node.js entwickelt sich kontinuierlich weiter und bietet Entwicklerinnen und Entwicklern effizientere Möglichkeiten für den Umgang mit Dateien und Pfaden. Eine wichtige Neuerung für Node.js-Entwickler wurde mit den Versionen Node.js v20.11.0 und Node.js v21.2.0 eingeführt: die integrierte Unterstützung für import.meta.dirname und import.meta.filename.

import.meta.filename und import.meta.dirname in Node.js verwenden

Dank der Einführung von import.meta.filename und import.meta.dirname ist dieser Vorgang jetzt viel einfacher. Sehen wir uns an, wie Sie mit den neuen Funktionen eine Konfigurationsdatei laden können.

Angenommen, Sie müssen eine YAML-Konfigurationsdatei laden, die sich im selben Verzeichnis wie Ihre JavaScript-Datei befindet. So geht's:

import fs from 'fs';

const { dirname: __dirname, filename: __filename } = import.meta;
const projectSetup = fs.readFileSync(`${__dirname}/setup.yml`, "utf8");

console.log(projectSetup);

In diesem Beispiel verwenden wir import.meta.dirname, um den Verzeichnisnamen der aktuellen Datei abzurufen, und weisen ihn der Variablen __dirname zu – für Kompatibilität mit den Namenskonventionen von CommonJS.

8. Native Timer-Promises in Node.js

Node.js, eine beliebte JavaScript-Laufzeitumgebung auf Basis der JavaScript-Engine V8 von Chrome, erleichtert Entwicklerinnen und Entwicklern mit regelmäßigen Updates und neuen Funktionen seit jeher die Arbeit.

Obwohl Node.js bereits mit Version v15 die native Verwendung von Timern mit einer Promise-Syntax eingeführt hat, muss ich zugeben, dass ich sie bisher nicht regelmäßig verwendet habe.

Die Timer setTimeout() und setInterval() in JavaScript: ein kurzer Überblick

Bevor wir uns mit nativen Timer-Promises befassen, sehen wir uns kurz die JavaScript-Timer setTimeout() und setInterval() an.

Die API setTimeout() ist eine JavaScript-Funktion, die nach Ablauf des Timers eine Funktion oder einen angegebenen Codeabschnitt ausführt.

setTimeout(function(){ 
    console.log("Hello World!"); 
}, 3000);

Im obigen Code wird „Hello World!“ nach drei Sekunden (3000 Millisekunden) in der Konsole ausgegeben.

setInterval() führt die angegebene Funktion dagegen wiederholt aus, wobei zwischen den einzelnen Aufrufen eine Verzögerung liegt.

setInterval(function(){ 
    console.log("Hello again!"); 
}, 2000);

Im obigen Code wird „Hello again!“ alle zwei Sekunden (2000 Millisekunden) in der Konsole ausgegeben.

Die bisherige Methode: setTimeout() in ein Promise einschließen

Früher mussten Entwicklerinnen und Entwickler die Funktion setTimeout() oft in ein Promise einschließen, um sie asynchron zu verwenden. So ließ sich setTimeout() mit async/await nutzen.

So sah das beispielsweise aus:

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();

Damit würde „Taking a break...“ ausgegeben, zwei Sekunden gewartet und anschließend „Two seconds later...“ ausgegeben.

Das funktionierte zwar, machte den Code aber unnötig komplex.

Native Timer-Promises in Node.js: der einfachere Weg

Mit den nativen Timer-Promises von Node.js müssen wir setTimeout() nicht mehr in ein Promise einschließen. Stattdessen können wir setTimeout() direkt mit async/await verwenden. Das macht den Code übersichtlicher, besser lesbar und einfacher zu warten. So verwenden Sie native Timer-Promises in Node.js:

const {
  setTimeout,
} = require('node:timers/promises');

setTimeout(2000, 'Two seconds later...').then((res) => {
  console.log(res);  
});

console.log('Taking a break...');

Im obigen Code wird setTimeout() aus node:timers/promises importiert. Anschließend verwenden wir es direkt mit async/await. Dadurch wird „Taking a break...“ ausgegeben, zwei Sekunden gewartet und dann „Two seconds later...“ ausgegeben.

Das vereinfacht die asynchrone Programmierung erheblich und macht den Code leichter lesbar, schreibbar und wartbar.

9. Berechtigungsmodell von Node.js

Rafael Gonzaga, inzwischen Mitglied des Node.js TSC, hat die Arbeit am Berechtigungsmodul von Node.js wieder aufgenommen. Ähnlich wie bei Deno bietet es konfigurierbare Einschränkungen für Ressourcen auf Prozessebene.

Angesichts von Supply-Chain-Sicherheitsrisiken, schädlichen npm-Paketen und anderen Bedrohungen wird es immer wichtiger, aus Sicherheits- und Compliance-Gründen die Ressourcen zu verwalten und zu kontrollieren, auf die Ihre Node.js-Anwendungen zugreifen können.

Node.js hat in diesem Zusammenhang eine experimentelle Funktion namens Berechtigungsmodul eingeführt, mit der sich Ressourcenzugriffe in Node.js-Anwendungen verwalten lassen. Aktiviert wird sie mit dem Kommandozeilen-Flag --experimental-permission.

Das Modell für Ressourcenberechtigungen in Node.js

Das Berechtigungsmodell von Node.js bietet eine Abstraktion zur Verwaltung des Zugriffs auf verschiedene Ressourcen, etwa Dateisysteme, Netzwerke, Umgebungsvariablen und Worker-Threads. Die Funktion ist besonders nützlich, wenn Sie die Ressourcen beschränken möchten, auf die ein bestimmter Teil Ihrer Anwendung zugreifen kann.

Zu den gängigen Ressourceneinschränkungen, die Sie mit dem Berechtigungsmodell festlegen können, gehören:

  • Lese- und Schreibzugriff auf das Dateisystem mit --allow-fs-read=* und --allow-fs-write=*. Sie können Verzeichnisse und konkrete Dateipfade angeben und mehrere Ressourcen zulassen, indem Sie die Flags wiederholen.

  • Aufrufe von Child-Prozessen mit --allow-child-process

  • Aufrufe von Worker-Threads mit --allow-worker

Das Berechtigungsmodell von Node.js bietet außerdem über process.permission.has(resource, value) eine Runtime-API, mit der sich bestimmte Zugriffsrechte abfragen lassen.

Wenn Sie versuchen, auf nicht freigegebene Ressourcen zuzugreifen – beispielsweise die Datei .env zu lesen –, wird der Fehler ERR_ACCESS_DENIED ausgegeben:

> 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 v21.6.1

Beispiel für das Berechtigungsmodell von Node.js

Stellen Sie sich eine Node.js-Anwendung vor, die Datei-Uploads verarbeitet. Sie möchten diesen Teil der Anwendung so einschränken, dass er nur auf ein bestimmtes Verzeichnis zugreifen kann, in dem die hochgeladenen Dateien gespeichert sind.

Aktivieren Sie beim Starten Ihrer Node.js-Anwendung die experimentelle Berechtigungsfunktion mit dem Flag --experimental-permission.

node --experimental-permission ./app.js

Außerdem möchten wir der Anwendung ausdrücklich erlauben, zwei vertrauenswürdige Dateien zu lesen: .env und setup.yml. Daher müssen wir die obige Angabe wie folgt anpassen:

node --experimental-permission --allow-fs-write=/tmp/uploads --allow-fs-read=.env --allow-fs-read=setup.yml ./app.js

Versucht die Anwendung auf diese Weise, außerhalb des angegebenen Upload-Pfads auf dateibasierte Systemressourcen zuzugreifen, wird sie mit einem Fehler abgebrochen.

Das folgende Codebeispiel zeigt, wie Sie einen Ressourcenzugriff mit try/catch umschließen und außerdem die Runtime-API für Berechtigungen von Node.js nutzen können. So lässt sich der Zugriff prüfen, ohne dass eine Ausnahme ausgelöst wird:

 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);
}

Beachten Sie, dass die Berechtigungsfunktion in Node.js weiterhin experimentell ist und sich noch ändern kann.

Zum Thema Berechtigungen und Sicherheitskonventionen für den Produktionseinsatz finden Sie weitere Informationen zum Entwickeln sicherer Node.js-Anwendungen in diesen Blogbeiträgen von Snyk:

Diese Beiträge bieten eine umfassende Anleitung zum Erstellen sicherer Container-Images für Node.js-Webanwendungen – ein entscheidender Schritt bei der Entwicklung sicherer Node.js-Anwendungen.

10. Node.js-Policy-Modul

Das Node.js-Policy-Modul ist eine Sicherheitsfunktion, die verhindern soll, dass schädlicher Code in eine Node.js-Anwendung geladen und ausgeführt wird. Es verfolgt zwar nicht die Herkunft des geladenen Codes zurück, bietet aber einen wirksamen Schutz vor potenziellen Bedrohungen.

Das Policy-Modul nutzt das CLI-Flag --experimental-policy, um das richtlinienbasierte Laden von Code zu aktivieren. Als Argument erwartet das Flag eine Policy-Manifestdatei im JSON-Format, zum Beispiel --experimental-policy=policy.json.

Die Policy-Manifestdatei enthält die Richtlinien, die Node.js beim Laden von Modulen befolgt. So lässt sich zuverlässig steuern, welche Art von Code in Ihre Anwendung geladen wird.

Das Node.js-Policy-Modul implementieren: Schritt-für-Schritt-Anleitung

Anhand eines einfachen Beispiels zeigen wir Ihnen, wie Sie das Node.js-Policy-Modul verwenden:

1. Erstellen Sie eine Policy-Datei. Die Datei sollte im JSON-Format vorliegen und die Richtlinien Ihrer App zum Laden von Modulen enthalten. Nennen wir sie policy.json.

Zum Beispiel:

    {
      "resources": {
        "./moduleA.js": {
          "integrity": "sha384-xxxxx"
        },
        "./moduleB.js": {
          "integrity": "sha384-yyyyy"
        }
      }
    }

In dieser Policy-Datei ist festgelegt, dass moduleA.js und moduleB.js bestimmte Integritätswerte aufweisen müssen, damit sie geladen werden.

Eine Policy-Datei für alle direkten und transitiven Abhängigkeiten zu erstellen, ist jedoch nicht einfach. Vor einigen Jahren entwickelte Bradley Meck das npm-Paket node-policy, das eine CLI zur automatischen Generierung der Policy-Datei bereitstellt.

2. Führen Sie Ihre Node.js-Anwendung mit dem --experimental-policy -Flag aus:

  node --experimental-policy=policy.json app.js

Mit diesem Befehl weisen Sie Node.js an, beim Laden der Module in app.js die in policy.json festgelegten Richtlinien zu befolgen.

3. Um Manipulationen an der Policy-Datei zu verhindern, können Sie mit dem --policy-integrity -Flag einen Integritätswert für die Datei selbst angeben:

    node --experimental-policy=policy.json --policy-integrity="sha384-zzzzz" app.js

Mit diesem Befehl wird sichergestellt, dass die Integrität der Policy-Datei gewahrt bleibt – auch wenn die Datei auf dem Datenträger geändert wird.

Einschränkungen der Integritätsrichtlinien in Node.js

Die Node.js-Runtime bietet keine integrierten Funktionen zum Generieren oder Verwalten der Policy-Datei. Das kann zu Schwierigkeiten führen, etwa bei der Verwaltung unterschiedlicher Richtlinien für Produktions- und Entwicklungsumgebungen sowie bei dynamischen Modulimporten.

Eine weitere Einschränkung: Wenn Sie bereits ein schädliches npm-Paket in seiner aktuellen Form verwenden, ist es zu spät, eine Modul-Integritätsrichtliniendatei zu generieren.

Ich empfehle Ihnen, die Entwicklungen in diesem Bereich zu verfolgen und die Funktion nach und nach einzuführen.

Weitere Informationen zum Node.js-Policy-Modul finden Sie im Artikel Einführung experimenteller Integritätsrichtlinien für Node.js. Er enthält eine ausführlichere Schritt-für-Schritt-Anleitung zur Arbeit mit der Policy-Integrität in Node.js.

Fazit

Bei einem Blick auf die modernen Node.js-Runtime-Funktionen, die Sie 2024 nutzen sollten, wird deutlich: Sie sollen Ihren Entwicklungsprozess vereinfachen, die Anwendungsleistung verbessern und die Sicherheit erhöhen. Diese Funktionen sind nicht nur ein Trend. Sie haben das Potenzial, unsere Herangehensweise an die Node.js-Entwicklung grundlegend zu verändern.

Stärken Sie die Sicherheit Ihrer Node.js-Anwendungen mit Snyk

Diese Node.js-Funktionen können Ihren Entwicklungsprozess und die Anwendungsleistung deutlich verbessern. Dennoch ist es wichtig, potenzielle Sicherheitsbedrohungen im Blick zu behalten. Snyk unterstützt Sie dabei: Mit diesem leistungsstarken Tool finden und beheben Sie bekannte Schwachstellen in Ihren Node.js-Abhängigkeiten und sorgen für ein sicheres Entwicklungsökosystem.

Nutzen Sie die Möglichkeiten von Snyk: Registrieren Sie sich hier kostenlos und starten Sie in eine sicherere Node.js-Entwicklung.

Gepostet in: