1.1 Tworzenie aplikacji konsolowych
Aplikacje konsolowe (CLI applications) - to takie który uruchamiamy poprzez terminal, tradycyjnie precyzujemy działanie takich poleceń poprzez przekazywanie parametrów przy ich wywoływaniu. Skrót CLI (command-line interface) tłumaczy się jako 'wiersz poleceń', ale dla niektórych systemów operacyjnych lub programów mówimy też właśnie o terminalu (np. dla VSC lub MacOS).
W poprzednim module zauważyliśmy, że przekazane parametry przy uruchomieniu skryptu są dostępne w tablicy process.args w postaci: ["/path/to/node", "/path/to/yourScript.js", "param 1", "param 2", …].
Aby otrzymać same parametry należy więc wykonać polecenie process.argv.slice(2), który zwróci tablicę ze wszystkimi rozdzielonymi spacjami wartościami: ["param 1", "param 2", …].
Opracowywanie wszelkiego rodzaju kombinacji parametrów i ich formatów jest bardzo niewygodne. Z tego powodu zazwyczaj wykorzystuje się dodatkowe moduły npm. Jeden z najpopularniejszych, którego będziemy używać, to moduł commander.
Dodatkowo możemy wykorzystać wprowadzanie danych w konsoli przez użytkownika zgodnie poprzez schemat pytanie-odpowiedź, będziemy do tego wykorzystywać moduł Node.js readline. Inicjalizacja jest dość prosta:
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,// wprowadzenie ze standardowego strumienia
output: process.stdout,// wyprowadzenie do standardowego strumienia
});
Podpinamy moduł readline i tworzymy instancję rl, dla której w opcjach przekazujemy strumienie wejścia i wyjścia, do wyboru may konsolę, plik i tak dalej. W naszym przypadku bierzemy standardowe strumienie i będziemy pracować używając konsoli, gdzie uruchamiamy skrypt. Jeśli chcemy zareagować na każdą wprowadzoną przez użytkownika wartość (potwierdzoną klawiszem enter) użyjemy zdarzenia line:
rl.on('line', cmd => {
console.log(`You just typed: ${cmd}`);
});
Nas jednak bardziej interesuje możliwość zadania użytkownikowi pytania i otrzymania na nie odpowiedzi, analogicznie do funkcji prompt z przeglądarki:
rl.question('Jak się nazywasz? ', answer => {
console.log(`Miło cię poznać. ${answer}`);
});
W trakcie trwania dłuższej operacji możemy ustawić w rozmowie pauzę czyli innymi słowami zablokować wprowadzenie kolejnych danych:
rl.pause();
Aby zamknąć wprowadzanie danych, należy wywołać metodę:
rl.close();
Napiszmy teraz prostą aplikację - "Odgadnij liczbę", gdzie należy zgadywać, jaką liczbę od 1 do 10 wylosował program, a ten na końcu pokaże, za którym razem nam się to udało i zapisze udaną próbę do pliku podanego jako parametr.
Przydadzą się nam standardowe moduły fs, readline oraz niestandardowe, które należy zainstalować przy pomocy npm, moduł [commander](https://www.npmjs.com/package/commander) i [colors](https://www.npmjs.com/package/colors). Poniżej znajdziecie kod programu i jego analizę.
const readline = require("readline");
const fs = require("fs").promises;
const { program } = require("commander");
require("colors");
program.option(
"-f, --file [type]",
"file for saving game results",
"results.txt"
);
program.parse(process.argv);
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
let count = 0;
const logFile = program.opts().file;
const mind = Math.floor(Math.random() * 10) + 1;
const isValid = (value) => {
if (isNaN(value)) {
console.log("Wprowadź liczbę!".red);
return false;
}
if (value < 1 || value > 10) {
console.log("Liczba powinna znajdować się w przedziale od 1 do 10".red);
return false;
}
return true;
};
const log = async (data) => {
try {
await fs.appendFile(logFile, `${data}\n`);
console.log(`Udało się zapisać rezultat w pliku ${logFile}`.green);
} catch (err) {
console.log(`Nie udało się zapisać pliku ${logFile}`.red);
}
};
const game = () => {
rl.question(
"Wprowadź liczbę od 1 do 10, aby zgadywać: ".yellow,
async (value) => {
value = Number.parseInt(value, 10);
if (!isValid(value)) {
game();
return;
}
count += 1;
if (value === mind) {
console.log("Gratulacje. Odgadłeś liczbę za %d razem".green, count);
await log(
`${new Date().toLocaleDateString()}: Gratulacje. Odgadłeś liczbę za ${count} razem`
);
rl.close();
return;
}
console.log("Nie zgadłeś. Kolejna próba.".red);
game();
}
);
};
game();
Cały program składa się z trzech funkcji. Podstawowa z nich to funkcja gry game(), która wywołuje się rekurencyjnie do momentu, aż nie zgadniemy podanej liczby. Na początku podłączamy moduł colors, który pozwala nam zmieniać kolor tekstu w konsoli. Dalej podłączamy moduł commander, który pozwoli nam obsłużyć parametry podane przy uruchomieniu programu
const { program } = require('commander');
program.option(
'-f, --file [type]',
'file for saving game results',
'results.txt',
);
program.parse(process.argv);
Wskazujemy, że opcjonalnie czekamy na wprowadzenie parametrów z flagą -f lub jej dłuższym zapisem --file. Innymi słowami określamy, że uruchomienie programu powinno nastąpić w następującej postaci:
node game.js -f my_log.txt
Kiedy w ten sposób uruchomimy nasz program, przekażemy, że należy w zmiennej program.file umieścić wartość my_log.txt.
W kodzie wskazujemy przez trzeci parametr program.option, że jeżeli parametr -f nie będzie przekazany przy uruchomieniu, to domyślnie program.file będzie miał wartość results.txt.
Drugi parametr to podpowiedź która pokaże się jeśli spróbujemy uruchomić nasz program z flagą -h czyli help
node index.js -h
Dalej w naszym programie wykonujemy inicjalizację modułu readline.
Tworzymy również trzy zmienne, które będziemy dalej wykorzystywać:
- count - to licznik ilości prób, które podjął użytkownik, aby odgadnąć liczbę,
- logFile - nazwa pliku, w którym będą zapisane rezultaty gry,
- mind - to wylosowana liczba od 1 do 10, którą należy odgadnąć.
Funkcja isValid odpowiada za walidację wprowadzonych wartości w konsoli. Sprawdza, czy wprowadzona wartość jest liczbą i znajduje się w przedziale od 1 do 10. Jeżeli dane są poprawne, to funkcja zwraca prawdę, jeżeli nie - fałsz.
Funkcja log odpowiada za zapisanie wyników gry. Wykorzystuje ona funkcję appendFile modułu fs do zapisu danych. Jeżeli plik istnieje, to rezultaty będą dopisane w istniejącym pliku, jeżeli pliku nie ma - zostanie on utworzony. Zwróć uwagę, że funkcja jest asynchroniczna i czekamy w niej na wykonanie operacji zapisania wyników.
Następnie docieramy do podstawowej funkcji game. Wewnątrz zachodzi wywołanie metody:
rl.question(
'Wprowadź liczbę od 1 do 10, aby zgadnąć wybraną: '.yellow,
(value) => {...});
która podsłuchuje konsolę i przy wprowadzeniu wartości wywołuje funkcję callback, która opracowuje wprowadzoną wartość.
Jeżeli wartość po sparsowaniu nie przechodzi walidacji, to włączamy funkcję gry ponownie:
value = Number.parseInt(value, 10);
if (!isValid(value)) {
game();
return;
}
- Jeśli walidacja przebiegła pomyślnie, to zwiększamy licznik prób do 1.
- Następnie porównujemy wprowadzoną wartość z "założoną" i jeśli tak to:
- Wyprowadzamy gratulacje i ilość prób w grze,
- Następnie przy pomocy funkcji log zapisujemy wynik w pliku,
- Po próbie zapisania pliku zamykamy interfejs do wprowadzania przez rl.close(),
- Nasz program kończy się
- Jeżeli jednak odpowiedź nie pokrywa się, wykonujemy ponownie funkcję game() do momentu w którym liczba zostanie odgadnięta.