Aufwärmen · Aufgabe 1 von 7
// B3.3 · ca. 34 Min. · Einstieg
Schreiben, Anhängen und atomares Ersetzen
Nach dieser Lektion schreiben Sie Dateien, hängen an, verhindern Überschreiben, speichern lesbares JSON und ersetzen eine Datei so, dass sie nie halb geschrieben ist.
Danach können Sie
- Eine Datei mit writeFile ersetzen, mit appendFile ergänzen und mit dem Flag wx Überschreiben verweigern
- Einen Wert als lesbares JSON speichern, in einem Ordner, den mkdir anlegt, wenn er fehlt
- Eine Datei atomar ersetzen: eine Temp-Datei im selben Ordner schreiben und sie dann über das Ziel umbenennen
Vorhersagen · Aufgabe 2 von 7
Sagen Sie es vorher, bevor Sie weiterlesen. notes.txt enthält die Zeile buy milk. Was gibt dieses Programm aus?
import {readFile, writeFile} from 'node:fs/promises'; await writeFile('notes.txt', 'call Ada\n'); console.log(await readFile('notes.txt', 'utf8'));Üben · Aufgabe 3 von 7
app.log enthält schon started. Setzen Sie die Funktion ein, damit stopped als zweite Zeile dazukommt und started bleibt.
await ____("app.log", "stopped\n");await ("app.log", "stopped\n");Üben · Aufgabe 4 von 7
f.txt enthält old. Ordnen Sie jedem Flag von writeFile("f.txt", "new\n", {flag}) zu, was passiert.
Üben · Aufgabe 5 von 7
Setzen Sie das dritte Argument ein, damit settings.json pro Ebene um zwei Leerzeichen eingerückt ist und mit einem Zeilenumbruch endet.
await writeFile("settings.json", JSON.stringify(settings, null, ____) + "\n");await writeFile("settings.json", JSON.stringify(settings, null, ) + "\n");Denksport · Aufgabe 6 von 7
Knobelaufgabe. settings.json enthält {"theme": "light"}. Das Programm unten speichert die neuen Einstellungen über eine Temp-Datei, stirbt aber vor dem rename. Was enthält settings.json danach?
import {rename, writeFile} from 'node:fs/promises'; await writeFile('.settings.json.tmp', '{"theme": "dark"}\n'); throw new Error('power cut'); // the program dies here await rename('.settings.json.tmp', 'settings.json');Anwenden · Aufgabe 7 von 7
Mini-Aufgabe. Schreiben Sie main.js so, dass node main.js "buy milk" die Notiz in data/notes.json neben main.js ablegt, ein JSON-Array mit zwei Leerzeichen Einrückung. Fehlen Ordner oder Datei, beginnt einfach eine leere Liste. Ersetzen Sie notes.json über eine Temp-Datei und rename, hängen Sie eine Zeile added: buy milk an data/notes.log an und geben Sie aus, wie viele Notizen es gibt. Führen Sie es zweimal aus und öffnen Sie beide Dateien.
Prüfen Sie Ihr Ergebnis anhand dieser Liste
Selbst programmieren
Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.
Ausgearbeitetes Beispiel
Ein Log, eine Weigerung und eine Einstellungsdatei
Dieses Programm legt den Ordner out neben main.js an und schreibt dort ein Log: writeFile beginnt es, appendFile fügt zwei Zeilen an. Ein zweites writeFile mit dem Flag wx weigert sich, das Log zu überschreiben, und meldet EEXIST. Zuletzt speichert es Einstellungen als eingerücktes JSON auf die sichere Art, über eine Temp-Datei und rename, und listet den Ordner: Keine Temp-Datei bleibt übrig. Führen Sie es zweimal aus, bleibt die Ausgabe gleich, weil writeFile das Log neu beginnt.
main.js
import {appendFile, mkdir, readFile, readdir, rename, writeFile} from 'node:fs/promises';
import {join} from 'node:path';
const dir = join(import.meta.dirname, 'out');
await mkdir(dir, {recursive: true}); // fine if out already exists
// writeFile replaces, appendFile adds.
const log = join(dir, 'app.log');
await writeFile(log, 'started\n');
await appendFile(log, 'saved settings\n');
await appendFile(log, 'stopped\n');
console.log((await readFile(log, 'utf8')).trimEnd());
// 'wx' refuses to overwrite an existing file.
try {
await writeFile(log, 'oops\n', {flag: 'wx'});
} catch (error) {
console.log('wx:', error.code);
}
// JSON, replaced in one step: temp file in the same folder, then rename.
const settings = join(dir, 'settings.json');
const temp = join(dir, `.settings.json.${process.pid}.tmp`);
await writeFile(temp, JSON.stringify({theme: 'dark', fontSize: 14}, null, 2) + '\n');
await rename(temp, settings);
console.log((await readFile(settings, 'utf8')).trimEnd());
console.log('in out:', (await readdir(dir)).sort().join(', '));
Ausführen mit
node main.jsAusgabe
started
saved settings
stopped
wx: EEXIST
{
"theme": "dark",
"fontSize": 14
}
in out: app.log, settings.json- appendFile hat zwei Zeilen nach started angefügt, weil jedes Datenstück mit "\n" endet.
- Mit {flag: 'wx'} lehnt writeFile mit EEXIST ab, und app.log behält seine drei Zeilen.
- JSON.stringify(value, null, 2) hat jede Eigenschaft in eine eigene Zeile gesetzt, zwei Leerzeichen tief.
- Die Temp-Datei wurde über settings.json umbenannt, also enthält der Ordner nur die zwei echten Dateien.
Übungen
Übung 1 von 2
Ein Temp-Name neben dem Ziel
Vor einem atomaren Ersetzen brauchen Sie einen Temp-Pfad im selben Ordner wie das Ziel. Schreiben Sie tempPathFor(target, id): Setzen Sie einen Punkt vor den Dateinamen und .<id>.tmp dahinter, und lassen Sie den Ordnerteil unverändert. "data/notes.json" mit id 42 ergibt "data/.notes.json.42.tmp"; "notes.json" ergibt ".notes.json.42.tmp". Pfade verwenden hier /. Dieser Teil ist reine String-Arbeit, er läuft also auch im Browser.
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.
Hinweise
Hinweis 1
target.lastIndexOf("/") findet den letzten Schrägstrich; ohne einen ist es -1.
Hinweis 2
Plus 1 ergibt den Index, an dem der Dateiname beginnt, auch 0, wenn es keinen Schrägstrich gibt.
Hinweis 3
target.slice(0, cut) ist der Ordnerteil mit Schrägstrich, target.slice(cut) der Dateiname.
Eine Lösung zeigen
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
// The temp path for an atomic replace: same folder, a dot before the name,
// .<id>.tmp after it. "data/notes.json", 42 -> "data/.notes.json.42.tmp"
export function tempPathFor(target, id) {
const cut = target.lastIndexOf("/") + 1;
return target.slice(0, cut) + "." + target.slice(cut) + "." + id + ".tmp";
}
console.log(tempPathFor("data/notes.json", 42));
Auf dem eigenen Computer ausführen
Installieren Sie Node.js 24 LTS oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.js
// The temp path for an atomic replace: same folder, a dot before the name,
// .<id>.tmp after it. "data/notes.json", 42 -> "data/.notes.json.42.tmp"
export function tempPathFor(target, id) {
return target + ".tmp";
}
console.log(tempPathFor("data/notes.json", 42));
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {tempPathFor} from './main.js';
test('eine Datei in einem Ordner behält ihren Ordner', () => {
const got = tempPathFor('data/notes.json', 42);
assert.equal(got, 'data/.notes.json.42.tmp', `tempPathFor lieferte ${JSON.stringify(got)}`);
});
test('eine Datei ohne Ordner', () => {
const got = tempPathFor('notes.json', 7);
assert.equal(got, '.notes.json.7.tmp', `tempPathFor lieferte ${JSON.stringify(got)}`);
});
test('ein tiefer absoluter Pfad', () => {
const got = tempPathFor('/home/ada/app/data/notes.json', 1234);
assert.equal(got, '/home/ada/app/data/.notes.json.1234.tmp', `tempPathFor lieferte ${JSON.stringify(got)}`);
});
package.json
{
"type": "module"
}
package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.
Programm ausführen:
node main.jsPrüfungen ausführen (learnrun.js muss im selben Ordner liegen):
node --testlearnrun.js herunterladenÜbung 2 von 2
saveJson, sicher
saveJson(path, value) schreibt value direkt nach path und scheitert, weil der Ordner data noch nicht existiert. Machen Sie es sicher: Legen Sie den Ordner von path mit mkdir und {recursive: true} an, schreiben Sie JSON.stringify(value, null, 2) plus "\n" in eine Temp-Datei in diesem Ordner, etwa .notes.json.<process.pid>.tmp, und benennen Sie die Temp-Datei dann über path um. Der Code darunter speichert zweimal und gibt Datei und Ordner aus. Starten Sie node main.js und prüfen Sie mit node --test.
Diese Übung braucht Node.js auf Ihrem Computer (die Browser-Version kann sie nicht ausführen). Dateien und Befehle stehen unten.
Hinweise
Hinweis 1
dirname(path) ist der Ordner; mkdir(dirname(path), {recursive: true}) tut nichts, wenn er schon existiert.
Hinweis 2
Bilden Sie den Temp-Pfad im selben Ordner: join(dir, `.${basename(path)}.${process.pid}.tmp`).
Hinweis 3
writeFile(temp, JSON.stringify(value, null, 2) + "\n"), dann rename(temp, path).
Eine Lösung zeigen
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
import {mkdir, readFile, readdir, rename, writeFile} from 'node:fs/promises';
import {basename, dirname, join} from 'node:path';
// Saves value as indented JSON, replacing path in one step.
export async function saveJson(path, value) {
const dir = dirname(path);
await mkdir(dir, {recursive: true});
const temp = join(dir, `.${basename(path)}.${process.pid}.tmp`);
await writeFile(temp, JSON.stringify(value, null, 2) + '\n');
await rename(temp, path);
}
const file = join(import.meta.dirname, 'data', 'notes.json');
await saveJson(file, {notes: ['buy milk']});
await saveJson(file, {notes: ['buy milk', 'call Ada']});
console.log((await readFile(file, 'utf8')).trimEnd());
console.log('ends with a newline:', (await readFile(file, 'utf8')).endsWith('\n'));
console.log('files in data:', (await readdir(dirname(file))).sort().join(', '));
Auf dem eigenen Computer ausführen
Installieren Sie Node.js 24 LTS oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.js
import {mkdir, readFile, readdir, rename, writeFile} from 'node:fs/promises';
import {basename, dirname, join} from 'node:path';
// Saves value as indented JSON, replacing path in one step.
export async function saveJson(path, value) {
await writeFile(path, JSON.stringify(value));
}
const file = join(import.meta.dirname, 'data', 'notes.json');
await saveJson(file, {notes: ['buy milk']});
await saveJson(file, {notes: ['buy milk', 'call Ada']});
console.log((await readFile(file, 'utf8')).trimEnd());
console.log('ends with a newline:', (await readFile(file, 'utf8')).endsWith('\n'));
console.log('files in data:', (await readdir(dirname(file))).sort().join(', '));
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {runMain} from './learnrun.js';
test('läuft und legt den Ordner data an', async () => {
await runMain();
});
test('notes.json enthält das zweite Speichern, um zwei Leerzeichen eingerückt', async () => {
const got = (await runMain()).split('\n').slice(0, 6).join('\n');
const expected = '{\n "notes": [\n "buy milk",\n "call Ada"\n ]\n}';
assert.equal(got, expected, `die Datei beginnt mit ${JSON.stringify(got)}`);
});
test('notes.json endet mit einem Zeilenumbruch', async () => {
const out = await runMain();
assert.match(out, /ends with a newline: true/, 'die Datei endet nicht mit "\\n"');
});
test('in data bleibt keine Temp-Datei übrig', async () => {
const last = (await runMain()).trimEnd().split('\n').at(-1);
assert.equal(last, 'files in data: notes.json', `der Ordner enthält: ${last}`);
});
package.json
{
"type": "module"
}
package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.
Programm ausführen:
node main.jsPrüfungen ausführen (learnrun.js muss im selben Ordner liegen):
node --testlearnrun.js herunterladenHäufige Fehler
Ein Objekt statt Text schreiben
import {writeFile} from 'node:fs/promises';
await writeFile('settings.json', {theme: 'dark'});
Was Node.js ausgibt
TypeError [ERR_INVALID_ARG_TYPE]: The "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received an instance of ObjectWarum, und die Lösung
writeFile schreibt Text oder Bytes, und seit v14 wandelt es andere Werte nicht mehr für Sie in Strings um. Machen Sie aus dem Wert zuerst JSON: writeFile("settings.json", JSON.stringify(settings, null, 2) + "\n").
In einen Ordner schreiben, den es nicht gibt
import {writeFile} from 'node:fs/promises';
await writeFile('data/notes.json', '[]\n');
Was Node.js ausgibt
Error: ENOENT: no such file or directory, open 'data/notes.json'Warum, und die Lösung
writeFile legt die Datei an, aber nicht die Ordner auf dem Weg dorthin. Legen Sie zuerst den Ordner an: await mkdir("data", {recursive: true}). Mit recursive entsteht jeder fehlende übergeordnete Ordner, und es scheitert nicht, wenn der Ordner schon da ist.
Auf den Ordner statt auf die Datei umbenennen
import {mkdir, rename, writeFile} from 'node:fs/promises';
await mkdir('data', {recursive: true});
await writeFile('.notes.json.tmp', '[]\n');
await rename('.notes.json.tmp', 'data'); // meant data/notes.json
Was Node.js ausgibt
Error: EISDIR: illegal operation on a directory, rename '.notes.json.tmp' -> 'data'Warum, und die Lösung
rename erwartet den vollständigen neuen Pfad der Datei, nicht den Ordner, in dem sie landen soll. rename überschreibt eine vorhandene Datei, aber liegt am neuen Pfad ein Ordner, gibt es stattdessen einen Fehler. Schreiben Sie rename(".notes.json.tmp", "data/notes.json").
JavaScript im Browser: die eigene Engine Ihres Browsers in einem abgeschotteten Worker. Syntaxfehler findet acorn 8.18.0, MIT. Lizenz und Quellcode
Abschlussquiz
5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.
Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.