Witaj w challengu Node.js, gdzie codziennie przez 7 dni zdobędziesz konkretną dawkę informacji dotyczących Node.js oraz wykorzystasz ją w praktyce. Pamiętaj żeby wykonywać dni challengu po kolei - od dnia pierwszego do ostatniego (dzięki temu Twoja wiedza będzie poukładana i kompletna).
Każdy dzień to jeden temat przewodni. W jego ramach stworzysz aplikację Node.js, która faktycznie będzie potrafiła coś zrobić - od razu zobaczysz wynik swojej pracy.
Kilka ważnych informacji
Przed przystąpieniem do rozwiązywania zadań przeczytaj poniższe wskazówki
Do pełnego i satysfakcjonującego doświadczania tego challengu jest potrzebna znajomość JavaScript z elementami ES6. Jeżeli potrzebujesz informacji z zakresu ES6 to znajdziesz je tutaj: tutorial ES6.
Poszczególne zadania rozwiązuj w odpowiednich plikach.
- Pierwszy dzień to wstęp do Twojej przygody z Node.js - dowiesz się w jaki sposób przygotować środowisko oraz jak pisać i testować programy Node.js.
- W kolejnych dniach dowiesz się w jaki sposób za pomocą Node.js wchodzić w interakcję z systemem operacyjnym (np. modyfikować pliki czy dokonywać szyfrowania).
- Druga część challengu jest poświęcona tworzeniu back-endu - dowiesz się jak stworzyć własny serwer.
- Pod koniec doświadczysz roli full-stack developera - stworzysz komunikujący się ze sobą front-end i back-end.
Wczoraj wykorzystywaliśmy funkcje, metody i zmienne globalne dostępne dla naszego programu "od ręki". Większość modułów należy natomiast dołączać za pomocą funkcji require('nazwa-modułu')
. Zwraca ona zazwyczaj obiekt z różnymi klasami, funkcjami, zmiennymi dostępnymi w module. Zazwyczaj przypisujemy go do zmiennej, np.:
const myModule = require('myModule');
Dobrą praktyką jest umieszczanie wszelkich
require()
na samej górze pliku.
Możesz znać ten zapis z front-endu, możesz również kojarzyć nowszy zapis z ES6 - import...
- jego działanie jest podobne do require
.
Jeżeli znasz składnię
import
to dobrze. Ale w Node.js stosujemyrequire()
. Obecnie nie ma bowiem bezpośredniego wsparcia dlaimport
. Takie wsparcie prawdopodobnie niedługo się pojawi, aleimport
będzie działał nieco odmiennie ze względu na specyfikę Node.
Jednym z wbudowanych w Node.js modułów jest fs
. Ta krótka nazwa pochodzi od ang. file system. Jak sama nazwa wskazuje - posłuży on nam do operacji na plikach. Dokładnie tego się dzisiaj nauczymy - będziemy odczytywać, zapisywać i sprawdzać pliki na dysku.
Aby wykorzystać moduł fs
będziemy od teraz na początku plików naszego programu pisali:
const fs = require('fs');
Pamiętaj o tym! Pod zmienną fs
będą się teraz kryły możliwości operacji na plikach. Nauczymy się teraz najważniejszych z nich:
Odczyt pliku odbywa się za pomocą asynchronicznej funkcji fs.readFile('nazwa pliku', opcje, funkcjaCallback)
.
Czas na kilka ważnych, mocno technicznych informacji. Najpierw trochę teorii - potem zobaczysz, że w praktyce wcale nie jest to takie straszne :)
Funkcja fs.readFile()
jak wiele innych w Node.js jest asynchroniczna. Oznacza to, że Node.js nie będzie czekał na zakończenie odczytu pliku - od razu przejdzie do kolejnej linijki kodu!
To bardzo duża zaleta Node.js, dlatego że niezależnie od tego czy plik ma np. 1KB czy 1TB, jego odczyt nie blokuje całego programu. Minusem jest natomiast to, że nie wystarczy pisać linijki pod linijką.
Trzeba wykorzystać funkcję callback - znasz tę koncepcję np. z AJAX-a, eventów czy funkcji czasu.
Możesz spotkać się z nazwą Node-style callback
lub podobną. Jest to nazwa pewnej konwencji przyjętej w Node.js: funkcja callback przyjmuje dwa parametry w tej kolejności:
- Pierwszy parametr to obiekt ewentualnego błędu. Jeżeli jest równy
null
to znaczy, że błędu nie było. W przeciwnym wypadku przechowuje informacje o błędzie. - Drugi argument to ewentualne dane zwrotne. Jeżeli nie wystąpi błąd to może zawierać np. w przypadku funkcji
fs.readFile()
zawartość pliku. Ten parametr może nie istnieć, np. dla zapisu pliku po prostu go omijamy.
Teraz praktyka - jak w końcu odczytać ten plik? Np. w ten sposób:
const fs = require('fs');
fs.readFile('/home/some/file.txt', 'utf8', (err, data) => {
if (err === null){
console.log('Poprawnie odczytano plik.', data);
} else {
console.log('Błąd podczas odczytu pliku!', err);
}
});
Zwróć uwagę na zapis. Dopiero po odczytaniu pliku Node.js wraca do naszej funkcji callback.
Następnie w środku sprawdzamy czy wystąpił błąd - możemy to zrobić porównując obiekt błędu do null
. null
oznacza brak błędu, cokolwiek innego - faktyczne wystąpienie jakiegoś problemu (np. plik nie istniał).
Jeżeli wolisz, tutaj masz wersję z komentarzami:
const fs = require('fs');
//Odczytaj plik...
fs.readFile('/home/some/file.txt', 'utf8', (err, data) => {//...kiedy skończysz uruchom naszą funkcję callback
if (err === null){ //Sprawdzamy czy obiekt błędu jest null-em - jeżeli tak - wyświetlamy zawartość pliku
console.log('Poprawnie odczytano plik.', data);
} else { //Jeżeli obiekt błędu nie był null-em, to pojawił się błąd - np. plik nie istniał
console.log('Błąd podczas odczytu pliku!', err);
}
});
Możesz też zauważyć, że jako środkowy parametr opcji przekazaliśmy 'utf8'
- sygnalizuje to Node-owi, że odczytujemy plik tekstowy i chcemy uzyskać z niego tekst.
Wiemy już w jaki sposób może wyglądać odczyt plików. A jak można zrealizować ich zapis lub modyfikacja?
Tutaj z pomocą przychodzi nam funkcja fs.writeFile('nazwa pliku', daneDoZapisania, funkcjaCallback)
.
W przypadku funkcji callbacku dla zapisu nie otrzymujemy żadnych danych zwrotnych - możemy zatem jedynie sprawdzić czy był jakiś błąd. Poniżej cały przykład zapisu:
const fs = require('fs');
fs.writeFile('./data/file.txt', 'Hello, World!', err => {
if (err === null){
console.log('Zapisano poprawnie!');
} else {
console.log('Błąd podczas zapisu pliku!', err);
}
});
Zwróć uwagę na zapis - jest on bardzo podobny do tego, który używaliśmy do odczytu pliku. Dopiero po zapisaniu pliku Node.js wraca do naszej funkcji callback.
Następnie w środku sprawdzamy czy wystąpił błąd - możemy to zrobić porównując obiekt błędu do null
. null
oznacza brak błędu, cokolwiek innego - faktyczne wystąpienie jakiegoś problemu (np. podaliśmy niepoprawną ścieżkę).
Jeżeli wolisz, tutaj masz wersję z komentarzami:
const fs = require('fs');
//Zapisz plik...
fs.writeFile('./data/file.txt', 'Hello, World!', err => {//...kiedy skończysz uruchom naszą funkcję callback
if (err === null){ //Sprawdzamy czy obiekt błędu jest null-em - jeżeli tak - wszystko poszło ok
console.log('Zapisano poprawnie!');
} else { //Jeżeli obiekt błędu nie był null-em, to pojawił się błąd - np. niepoprawna ścieżka
console.log('Błąd podczas zapisu pliku!', err);
}
});
Poniższy kod realizuje zadanie zapisu danych do pliku, a następnie ich odczyt:
const fs = require('fs');
//Zapisz plik...
fs.writeFile('./data/file.txt', 'Hello, World!', err => {//Zapis: ...kiedy skończysz uruchom naszą funkcję callback
if (err === null){ //Zapis: Sprawdzamy czy obiekt błędu jest null-em - jeżeli tak - wszystko poszło ok
//Odczytaj plik...
fs.readFile('./data/file.txt', 'utf8', (err, data) => {//Odczyt: ...kiedy skończysz uruchom naszą funkcję callback
if (err === null){ //Odczyt: Sprawdzamy czy obiekt błędu jest null-em - jeżeli tak - wyświetlamy zawartość pliku
console.log('Poprawnie zapisano i odczytano plik.', data);
} else { //Odczyt: Jeżeli obiekt błędu nie był null-em, to pojawił się błąd - np. plik nie istniał
console.log('Błąd podczas odczytu pliku!', err);
}
});
} else { //Zapis: Jeżeli obiekt błędu nie był null-em, to pojawił się błąd - np. niepoprawna ścieżka
console.log('Błąd podczas zapisu pliku!', err);
}
});
Zwróć szczególną uwagę na zagnieżdżenia - musimy pamiętać o odpowiedniej kolejności.
Kolejną ciekawą funkcją modułu fs
jest możliwość zlistowania plików i folderów znajdujących się w danym folderze.
Wykonujemy to za pomocą funkcji fs.readdir('ścieżka/do/folderu', funkcjaCallback)
.
Oto przykład wykorzystania - wyświetli on nazwy wszystkich plików i folderów w folderze /home/ja
:
const fs = require('fs');
fs.readdir('/home/ja', (err, files) => {
if (err === null){
console.log('Lista plików:');
files.forEach(file => {
console.log(file);
});
} else {
console.log('Błąd podczas listowania katalogu!', err);
}
});
Jak widzisz - zasady są tu takie same jak w przypadku innych asynchronicznych funkcji. Daną zwrotną, którą otrzymujemy w callbacku jest tutaj tablica nazw plików.
Jeżeli wolisz, tutaj masz wersję z komentarzami:
const fs = require('fs');
//Odczytaj listę plików i folderów...
fs.readdir('/home/ja', (err, files) => {//...kiedy skończysz uruchom naszą funkcję callback
if (err === null){ //Sprawdzamy czy obiekt błędu jest null-em - jeżeli tak - wszystko poszło ok
console.log('Lista plików:');
files.forEach(file => {
console.log(file);
});
} else {//Jeżeli obiekt błędu nie był null-em, to pojawił się błąd - np. niepoprawna ścieżka
console.log('Błąd podczas listowania katalogu!', err);
}
});
Moduł fs
dostarcza jeszcze szereg innych możliwości - np. pobierania informacji o dacie stworzenia czy rozmiarze pliku; śledzenie zmian w plikach; zaawansowane przetwarzanie plików i wiele innych. Dzisiaj poznaliśmy najważniejsze funkcje związane z plikami - zapis, odczyt i listowanie.
Ćwiczenia wykonuj w odpowiednich plikach. W folderze
app
są one ponumerowane tak samo jak poniżej - zadaniu1. Rozgrzewka
odpowiada plikapp/zadanie01.js
itd. Aby uruchomić zadanie podaj jego nazwę (pamiętaj, aby linia komend/terminal był otwarty na kataloguapp
tego repozytorium), np.:node ./zadanie01.js
Napisz program, w którym otworzysz plik znajdujący się w app/data/zadanie01/input.json
(z poziomu Twojego programu to będzie plik ./data/zadanie01/input.json
).
Jest to tablica liczb w formacie JSON. Aby ją rozkodować do tablicy JS skorzystaj z JSON.parse(daneWejsciowe)
.
Następnie zsumuj wszystkie liczby, a wynik zapisz tekstem do pliku app/data/zadanie01/sum.txt
.
Pamiętaj, że funkcje odczytu i zapisu są asynchronicze! Twój kod powinien odpowiadać takiemu pseudokodowi:
ODCZYT PLIKU
KIEDY SKOŃCZY - CALLBACK:
ZMIANA JSONA NA TABLICĘ
WYKONANIE SUMY
ZAPIS PLIKU
KIERY SKOŃCZY - CALLBACK:
INFORMACJA O POWODZENIU LUB BŁĘDZIE
Nie zapomnij o require()
dla odpowiedniego modułu.
Napisz program, który wyświetli zawartość wszystkich plików w folderze app/data/zadanie02/
.
Skorzystaj z możliwości listowania plików w folderze. Po otrzymaniu listy plików wyświetlaj je w pętli.
Pamiętaj, że funkcje listowania i odczytu są asynchronicze! Twój kod powinien odpowiadać takiemu pseudokodowi:
LISTOWANIE ZAWARTOŚCI KATALOGU
KIEDY SKOŃCZY - CALLBACK:
PĘTLA DLA KAŻDEJ NAZWY PLIKU
ODCZYT PLIKU
KIERY SKOŃCZY - CALLBACK:
WYŚWIETLENIE ZAWARTOŚCI PLIKU LUB INFORMACJI BŁĘDZIE
Dzisiejsze zadanie dnia pozwoli Ci stworzyć program "TrAwKa". Ma on modyfikować plik tekstowy w taki sposób, aby każdy nieparzysty znak był pisany wielką literą, a parzysty małą literą.
Efekt będzie nieco przypominał nieprzystrzyżony trawnik ;) Np. plik z zawartością Hello, World!
zamieni na HeLlO, wOrLd!
.
Wykorzystaj wiedzę z dnia pierwszego i spraw, aby nazwa pliku do modyfikacji mogła być podawana jako argument Twojego programu Node.
Pamiętaj, że funkcje odczytu i zapisu są asynchronicze! Twój kod powinien odpowiadać takiemu pseudokodowi:
ODCZYT PLIKU
KIEDY SKOŃCZY - CALLBACK:
ZMIANA TEKSTU WEDŁUG WZORU
ZAPIS TAKIEGO SAMEGO PLIKU
KIERY SKOŃCZY - CALLBACK:
INFORMACJA O POWODZENIU LUB BŁĘDZIE
Zawsze wykonuj kopię zapasową pliku na którym operujesz!
Do testu możesz posłużyć się plikiem w app/data/zadanieDnia/test.txt
np. w ten sposób:
node zadanieDnia.js ./data/zadanieDnia/test.txt
To wszystko na dziś - gratulacje! Do jutra :)