Aufwärmen · Aufgabe 1 von 7
Zum Aufwärmen aus Lektion I3.1. Was gibt node main.js aus?
// todos.js
export function count(todos) {
return todos.length;
}
// main.js
import {count} from "./todos.js";
console.log(count(["a", "b"]));// I3.5 · ca. 45 Min. · Aufbau
In diesem Projekt schreiben Sie ein To-do-Werkzeug, das Sie als node main.js add Buy milk starten: drei Module, eine JSON-Datei, die die Liste behält, und klare Fehler.
Lektion 5 von 5 in I3 Module, Node.js und npm
Danach können Sie
Aufwärmen · Aufgabe 1 von 7
// todos.js
export function count(todos) {
return todos.length;
}
// main.js
import {count} from "./todos.js";
console.log(count(["a", "b"]));Vorhersagen · Aufgabe 2 von 7
// main.js
console.log(process.argv.slice(2));Üben · Aufgabe 3 von 7
options: {file: {type: "____", default: "todos.json"}},Üben · Aufgabe 4 von 7
Üben · Aufgabe 5 von 7
// main.js
const saved = JSON.stringify([{id: 1, text: "Tea", done: false, added: new Date(0)}]);
const [todo] = JSON.parse(saved);
console.log(typeof todo.id, typeof todo.done, typeof todo.added);Denksport · Aufgabe 6 von 7
// main.js
console.error("No to-do with id 7");
process.exitCode = 1;
console.log("bye");Anwenden · Aufgabe 7 von 7
Prüfen Sie Ihr Ergebnis anhand dieser Liste
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
Vor der Kommandozeile sehen Sie todos.js und store.js in einem kurzen Skript. Es lädt eine Liste aus einer Datei, die es noch nicht gibt, fügt zwei To-dos hinzu, markiert eines als erledigt, speichert die Liste und liest sie so zurück, wie es ein späterer Lauf täte. In den Übungen bauen Sie main.js, die Kommandozeile um die beiden herum.
main.js
import {addTodo, markDone, formatList} from "./todos.js";
import {loadTodos, saveTodos} from "./store.js";
const file = "example.json";
let todos = await loadTodos(file); // [] the first time: no file yet
console.log(formatList(todos));
todos = addTodo(todos, "Buy milk");
todos = addTodo(todos, "Call Ada");
todos = markDone(todos, 1);
await saveTodos(file, todos);
// A later run would start here: read the file again.
console.log(formatList(await loadTodos(file)));
try {
markDone(todos, 7);
} catch (error) {
console.log(error.message);
}
todos.js
// A to-do is {id, text, done}. Every function returns a new array.
export function addTodo(todos, text) {
const id = todos.length === 0 ? 1 : Math.max(...todos.map((todo) => todo.id)) + 1;
return [...todos, {id, text, done: false}];
}
export function markDone(todos, id) {
if (!todos.some((todo) => todo.id === id)) throw new Error(`No to-do with id ${id}`);
return todos.map((todo) => (todo.id === id ? {...todo, done: true} : todo));
}
export function removeTodo(todos, id) {
if (!todos.some((todo) => todo.id === id)) throw new Error(`No to-do with id ${id}`);
return todos.filter((todo) => todo.id !== id);
}
export function formatList(todos) {
if (todos.length === 0) return "Nothing to do.";
return todos.map((todo) => `${todo.done ? "[x]" : "[ ]"} ${todo.id}. ${todo.text}`).join("\n");
}
store.js
import {readFile, writeFile} from "node:fs/promises";
// The list in the file, or an empty list if the file does not exist yet.
export async function loadTodos(file) {
try {
return JSON.parse(await readFile(file, "utf8"));
} catch (error) {
if (error.code === "ENOENT") return [];
throw error;
}
}
export async function saveTodos(file, todos) {
await writeFile(file, JSON.stringify(todos, null, 2) + "\n");
}
Ausführen mit
node main.jsAusgabe
Nothing to do.
[x] 1. Buy milk
[ ] 2. Call Ada
No to-do with id 7Übung 1 von 3
main.js ist die Kommandozeile des Werkzeugs, mit todos.js und store.js daneben. node main.js add Buy milk soll das To-do "Buy milk" hinzufügen, aber der Startcode behält nur das erste Wort. Korrigieren Sie die Zeile, die den Text baut. Die Tests nutzen ihre eigene --file, sie beginnen also mit einer leeren Liste.
Diese Übung braucht Node.js auf Ihrem Computer (die Browser-Version kann sie nicht ausführen). Dateien und Befehle stehen unten.
words enthält jedes Wort nach add. Welches davon nutzt der Startcode?
Verbinden Sie die Wörter mit einem Leerzeichen.
addTodo(todos, words.join(" "))
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
import {parseArgs} from "node:util";
import {addTodo, markDone, removeTodo, formatList} from "./todos.js";
import {loadTodos, saveTodos} from "./store.js";
const USAGE = "Usage: node main.js [--file todos.json] add <text> | list | done <id> | remove <id>";
const {values, positionals} = parseArgs({
options: {file: {type: "string", default: "todos.json"}},
allowPositionals: true
});
const [command, ...words] = positionals;
const todos = await loadTodos(values.file);
try {
if (command === "add" && words.length > 0) {
const next = addTodo(todos, words.join(" "));
await saveTodos(values.file, next);
console.log(`Added ${next.at(-1).id}. ${next.at(-1).text}`);
} else if (command === "list") {
console.log(formatList(todos));
} else if (command === "done" || command === "remove") {
const id = Number(words[0]);
const next = command === "done" ? markDone(todos, id) : removeTodo(todos, id);
await saveTodos(values.file, next);
console.log(`${command === "done" ? "Done" : "Removed"}: ${id}`);
} else {
console.error(USAGE);
process.exitCode = 1;
}
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
Installieren Sie ECMAScript 2026 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 {parseArgs} from "node:util";
import {addTodo, markDone, removeTodo, formatList} from "./todos.js";
import {loadTodos, saveTodos} from "./store.js";
const USAGE = "Usage: node main.js [--file todos.json] add <text> | list | done <id> | remove <id>";
const {values, positionals} = parseArgs({
options: {file: {type: "string", default: "todos.json"}},
allowPositionals: true
});
const [command, ...words] = positionals;
const todos = await loadTodos(values.file);
try {
if (command === "add" && words.length > 0) {
const next = addTodo(todos, words[0]);
await saveTodos(values.file, next);
console.log(`Added ${next.at(-1).id}. ${next.at(-1).text}`);
} else if (command === "list") {
console.log(formatList(todos));
} else if (command === "done" || command === "remove") {
const id = Number(words[0]);
const next = command === "done" ? markDone(todos, id) : removeTodo(todos, id);
await saveTodos(values.file, next);
console.log(`${command === "done" ? "Done" : "Removed"}: ${id}`);
} else {
console.error(USAGE);
process.exitCode = 1;
}
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {runMain} from './learnrun.js';
const file = 'test-' + Date.now() + '-' + Math.floor(Math.random() * 1e6) + '.json';
const todo = (...args) => runMain({args: ['--file', file, ...args]});
test('add Buy milk fügt ein To-do mit beiden Wörtern hinzu', async () => {
const got = await todo('add', 'Buy', 'milk');
assert.equal(got, 'Added 1. Buy milk\n', `add gab ${JSON.stringify(got)} aus`);
});
test('list zeigt es, noch nicht erledigt', async () => {
const got = await todo('list');
assert.equal(got, '[ ] 1. Buy milk\n', `list gab ${JSON.stringify(got)} aus`);
});
todos.js
// A to-do is {id, text, done}. Every function returns a new array.
export function addTodo(todos, text) {
const id = todos.length === 0 ? 1 : Math.max(...todos.map((todo) => todo.id)) + 1;
return [...todos, {id, text, done: false}];
}
export function markDone(todos, id) {
if (!todos.some((todo) => todo.id === id)) throw new Error(`No to-do with id ${id}`);
return todos.map((todo) => (todo.id === id ? {...todo, done: true} : todo));
}
export function removeTodo(todos, id) {
if (!todos.some((todo) => todo.id === id)) throw new Error(`No to-do with id ${id}`);
return todos.filter((todo) => todo.id !== id);
}
export function formatList(todos) {
if (todos.length === 0) return "Nothing to do.";
return todos.map((todo) => `${todo.done ? "[x]" : "[ ]"} ${todo.id}. ${todo.text}`).join("\n");
}
store.js
import {readFile, writeFile} from "node:fs/promises";
// The list in the file, or an empty list if the file does not exist yet.
export async function loadTodos(file) {
try {
return JSON.parse(await readFile(file, "utf8"));
} catch (error) {
if (error.code === "ENOENT") return [];
throw error;
}
}
export async function saveTodos(file, todos) {
await writeFile(file, JSON.stringify(todos, null, 2) + "\n");
}
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 3
node main.js done 1 soll To-do 1 als erledigt markieren, aber der Startcode antwortet No to-do with id 1. Die ID kommt als Text von der Kommandozeile, und die IDs in der Liste sind Zahlen. Korrigieren Sie die Zeile, die die ID liest. Eine unbekannte ID muss weiterhin mit einem Exit-Code ungleich 0 enden.
Diese Übung braucht Node.js auf Ihrem Computer (die Browser-Version kann sie nicht ausführen). Dateien und Befehle stehen unten.
Was ist typeof words[0]? Und womit vergleicht markDone es?
=== zwischen dem Text "1" und der Zahl 1 ist false.
const id = Number(words[0]);
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
import {parseArgs} from "node:util";
import {addTodo, markDone, removeTodo, formatList} from "./todos.js";
import {loadTodos, saveTodos} from "./store.js";
const USAGE = "Usage: node main.js [--file todos.json] add <text> | list | done <id> | remove <id>";
const {values, positionals} = parseArgs({
options: {file: {type: "string", default: "todos.json"}},
allowPositionals: true
});
const [command, ...words] = positionals;
const todos = await loadTodos(values.file);
try {
if (command === "add" && words.length > 0) {
const next = addTodo(todos, words.join(" "));
await saveTodos(values.file, next);
console.log(`Added ${next.at(-1).id}. ${next.at(-1).text}`);
} else if (command === "list") {
console.log(formatList(todos));
} else if (command === "done" || command === "remove") {
const id = Number(words[0]);
const next = command === "done" ? markDone(todos, id) : removeTodo(todos, id);
await saveTodos(values.file, next);
console.log(`${command === "done" ? "Done" : "Removed"}: ${id}`);
} else {
console.error(USAGE);
process.exitCode = 1;
}
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
Installieren Sie ECMAScript 2026 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 {parseArgs} from "node:util";
import {addTodo, markDone, removeTodo, formatList} from "./todos.js";
import {loadTodos, saveTodos} from "./store.js";
const USAGE = "Usage: node main.js [--file todos.json] add <text> | list | done <id> | remove <id>";
const {values, positionals} = parseArgs({
options: {file: {type: "string", default: "todos.json"}},
allowPositionals: true
});
const [command, ...words] = positionals;
const todos = await loadTodos(values.file);
try {
if (command === "add" && words.length > 0) {
const next = addTodo(todos, words.join(" "));
await saveTodos(values.file, next);
console.log(`Added ${next.at(-1).id}. ${next.at(-1).text}`);
} else if (command === "list") {
console.log(formatList(todos));
} else if (command === "done" || command === "remove") {
const id = words[0];
const next = command === "done" ? markDone(todos, id) : removeTodo(todos, id);
await saveTodos(values.file, next);
console.log(`${command === "done" ? "Done" : "Removed"}: ${id}`);
} else {
console.error(USAGE);
process.exitCode = 1;
}
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {runMain} from './learnrun.js';
const file = 'test-' + Date.now() + '-' + Math.floor(Math.random() * 1e6) + '.json';
const todo = (...args) => runMain({args: ['--file', file, ...args]});
test('done 2 markiert das zweite To-do', async () => {
await todo('add', 'Tea');
await todo('add', 'Bread');
const got = await todo('done', '2');
assert.equal(got, 'Done: 2\n', `done 2 gab ${JSON.stringify(got)} aus`);
});
test('list zeigt nur das zweite als erledigt', async () => {
const got = await todo('list');
assert.equal(got, '[ ] 1. Tea\n[x] 2. Bread\n', `list gab ${JSON.stringify(got)} aus`);
});
test('remove 1 entfernt das erste', async () => {
await todo('remove', '1');
const got = await todo('list');
assert.equal(got, '[x] 2. Bread\n', `nach remove 1 gab list ${JSON.stringify(got)} aus`);
});
test('done 9 endet mit einem Exit-Code ungleich 0', async () => {
await assert.rejects(todo('done', '9'), Error, 'done 9 endete mit Exit-Code 0');
});
todos.js
// A to-do is {id, text, done}. Every function returns a new array.
export function addTodo(todos, text) {
const id = todos.length === 0 ? 1 : Math.max(...todos.map((todo) => todo.id)) + 1;
return [...todos, {id, text, done: false}];
}
export function markDone(todos, id) {
if (!todos.some((todo) => todo.id === id)) throw new Error(`No to-do with id ${id}`);
return todos.map((todo) => (todo.id === id ? {...todo, done: true} : todo));
}
export function removeTodo(todos, id) {
if (!todos.some((todo) => todo.id === id)) throw new Error(`No to-do with id ${id}`);
return todos.filter((todo) => todo.id !== id);
}
export function formatList(todos) {
if (todos.length === 0) return "Nothing to do.";
return todos.map((todo) => `${todo.done ? "[x]" : "[ ]"} ${todo.id}. ${todo.text}`).join("\n");
}
store.js
import {readFile, writeFile} from "node:fs/promises";
// The list in the file, or an empty list if the file does not exist yet.
export async function loadTodos(file) {
try {
return JSON.parse(await readFile(file, "utf8"));
} catch (error) {
if (error.code === "ENOENT") return [];
throw error;
}
}
export async function saveTodos(file, todos) {
await writeFile(file, JSON.stringify(todos, null, 2) + "\n");
}
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 3 von 3
Ohne Befehl, bei einem unbekannten Befehl oder bei add ohne Wörter soll das Werkzeug die Usage-Zeile auf stderr ausgeben und mit Exit-Code 1 enden, damit ein aufrufendes Skript es merkt. Der Startcode gibt die Usage-Zeile mit console.log aus, und der Exit-Code bleibt 0. Korrigieren Sie den else-Zweig.
Diese Übung braucht Node.js auf Ihrem Computer (die Browser-Version kann sie nicht ausführen). Dateien und Befehle stehen unten.
Zwei Dinge: wohin die Usage-Zeile geht und der Exit-Code.
console.error schreibt auf stderr.
console.error(USAGE); dann process.exitCode = 1;
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
import {parseArgs} from "node:util";
import {addTodo, markDone, removeTodo, formatList} from "./todos.js";
import {loadTodos, saveTodos} from "./store.js";
const USAGE = "Usage: node main.js [--file todos.json] add <text> | list | done <id> | remove <id>";
const {values, positionals} = parseArgs({
options: {file: {type: "string", default: "todos.json"}},
allowPositionals: true
});
const [command, ...words] = positionals;
const todos = await loadTodos(values.file);
try {
if (command === "add" && words.length > 0) {
const next = addTodo(todos, words.join(" "));
await saveTodos(values.file, next);
console.log(`Added ${next.at(-1).id}. ${next.at(-1).text}`);
} else if (command === "list") {
console.log(formatList(todos));
} else if (command === "done" || command === "remove") {
const id = Number(words[0]);
const next = command === "done" ? markDone(todos, id) : removeTodo(todos, id);
await saveTodos(values.file, next);
console.log(`${command === "done" ? "Done" : "Removed"}: ${id}`);
} else {
console.error(USAGE);
process.exitCode = 1;
}
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
Installieren Sie ECMAScript 2026 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 {parseArgs} from "node:util";
import {addTodo, markDone, removeTodo, formatList} from "./todos.js";
import {loadTodos, saveTodos} from "./store.js";
const USAGE = "Usage: node main.js [--file todos.json] add <text> | list | done <id> | remove <id>";
const {values, positionals} = parseArgs({
options: {file: {type: "string", default: "todos.json"}},
allowPositionals: true
});
const [command, ...words] = positionals;
const todos = await loadTodos(values.file);
try {
if (command === "add" && words.length > 0) {
const next = addTodo(todos, words.join(" "));
await saveTodos(values.file, next);
console.log(`Added ${next.at(-1).id}. ${next.at(-1).text}`);
} else if (command === "list") {
console.log(formatList(todos));
} else if (command === "done" || command === "remove") {
const id = Number(words[0]);
const next = command === "done" ? markDone(todos, id) : removeTodo(todos, id);
await saveTodos(values.file, next);
console.log(`${command === "done" ? "Done" : "Removed"}: ${id}`);
} else {
console.log(USAGE);
}
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {runMain} from './learnrun.js';
const file = 'test-' + Date.now() + '-' + Math.floor(Math.random() * 1e6) + '.json';
const todo = (...args) => runMain({args: ['--file', file, ...args]});
test('kein Befehl endet mit einem Exit-Code ungleich 0', async () => {
await assert.rejects(todo(), Error, 'node main.js ohne Befehl endete mit Exit-Code 0');
});
test('add ohne Wörter endet mit einem Exit-Code ungleich 0', async () => {
await assert.rejects(todo('add'), Error, 'add ohne Wörter endete mit Exit-Code 0');
});
test('ein unbekannter Befehl endet mit einem Exit-Code ungleich 0', async () => {
await assert.rejects(todo('fly'), Error, 'der Befehl fly endete mit Exit-Code 0');
});
test('list funktioniert weiterhin und gibt Nothing to do. aus', async () => {
const got = await todo('list');
assert.equal(got, 'Nothing to do.\n', `list gab ${JSON.stringify(got)} aus`);
});
todos.js
// A to-do is {id, text, done}. Every function returns a new array.
export function addTodo(todos, text) {
const id = todos.length === 0 ? 1 : Math.max(...todos.map((todo) => todo.id)) + 1;
return [...todos, {id, text, done: false}];
}
export function markDone(todos, id) {
if (!todos.some((todo) => todo.id === id)) throw new Error(`No to-do with id ${id}`);
return todos.map((todo) => (todo.id === id ? {...todo, done: true} : todo));
}
export function removeTodo(todos, id) {
if (!todos.some((todo) => todo.id === id)) throw new Error(`No to-do with id ${id}`);
return todos.filter((todo) => todo.id !== id);
}
export function formatList(todos) {
if (todos.length === 0) return "Nothing to do.";
return todos.map((todo) => `${todo.done ? "[x]" : "[ ]"} ${todo.id}. ${todo.text}`).join("\n");
}
store.js
import {readFile, writeFile} from "node:fs/promises";
// The list in the file, or an empty list if the file does not exist yet.
export async function loadTodos(file) {
try {
return JSON.parse(await readFile(file, "utf8"));
} catch (error) {
if (error.code === "ENOENT") return [];
throw error;
}
}
export async function saveTodos(file, todos) {
await writeFile(file, JSON.stringify(todos, null, 2) + "\n");
}
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 herunterladenimport {readFile} from "node:fs/promises";
const todos = JSON.parse(await readFile("todos.json", "utf8"));
console.log(todos.length);
Was Node.js ausgibt
Error: ENOENT: no such file or directory, open 'todos.json'Warum, und die Lösung
Beim ersten Lauf gibt es noch keine Datei. Fangen Sie den Fehler ab und geben Sie eine leere Liste zurück, wenn error.code "ENOENT" ist; werfen Sie jeden anderen Fehler weiter, damit kaputtes JSON nicht stillschweigend durch eine leere Liste ersetzt wird. loadTodos aus store.js tut genau das.
import {readFile, writeFile} from "node:fs/promises";
await writeFile("todos.json", "");
const todos = JSON.parse(await readFile("todos.json", "utf8"));
console.log(todos.length);
Was Node.js ausgibt
SyntaxError: Unexpected end of JSON inputWarum, und die Lösung
JSON.parse braucht einen vollständigen JSON-Wert; ein leerer Text ist keiner. Schreiben Sie die Liste immer mit JSON.stringify, damit die Datei mindestens [] enthält. Wenn Menschen die Datei von Hand bearbeiten, fangen Sie den SyntaxError ab und sagen Sie, welche Datei kaputt ist, statt mit einer leeren Liste zu starten.
import {parseArgs} from "node:util";
const {values} = parseArgs({
args: ["--fil", "work.json"],
options: {file: {type: "string"}}
});
console.log(values.file);
Was Node.js ausgibt
TypeError [ERR_PARSE_ARGS_UNKNOWN_OPTION]: Unknown option '--fil'Warum, und die Lösung
parseArgs ist standardmäßig streng: Jede Option muss unter options deklariert sein, ein Tippfehler scheitert also sofort, statt ignoriert zu werden. Hier ist --fil ein Tippfehler für --file. Fangen Sie diesen Fehler in main.js ab und geben Sie die Usage-Zeile aus, oder setzen Sie strict: false, wenn Sie unbekannte Optionen wirklich akzeptieren wollen.
JavaScript im Browser: die eigene Engine Ihres Browsers in einem abgeschotteten Worker. Syntaxfehler findet acorn 8.18.0, MIT. Lizenz und Quellcode
5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.
Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.