Moduł 1 - Zajęcia 2 - Tworzenie aplikacji konsolowych

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.