w Praktyce
10 godz. 44 min · Node.js · Full-stack i Programowanie
Piotr PalarzWeb DeveloperW kursie tym poznasz platformę Node.js od podstaw, aż po bardziej zaawansowane koncepcje. Zaczniemy od omówienia czym jest Node, a także jakie może być zastosowanie tej technologii. Następnie przejdziemy przez proces instalacji i napisanie swojego pierwszego skryptu. Już na samym początku dokładnie omówimy tworzenie własnych modułów, gdyż jest to wiedza niezbędna, by dobrze zrozumieć funkcjonowanie Node. Następnie dowiesz się czym są zdarzenia, jak działa model “publish / subscribe”, a także czym jest “event-driven development”. Chwilę później omówimy pracę z buforami, stream’ami, a także ze standardowym wejściem i wyjściem. Ta wiedza pozwoli Ci zrozumieć jak tworzone aplikacje mogą otrzymywać od użytkownika dane oraz jak je wyświetlają.
Niezwykle ważnym rozdziałem będzie ten, poświęcony pracy z systemem plików. Szczególnie dla dewelopera, pracującego dotychczas z językiem JavaScript wyłącznie w środowisku przeglądarki internetowej. Node pozwala bowiem tworzyć nowe pliki, zapisywać w nich dane oraz odczytywać i usuwać pliki i katalogi. Przy wszystkich tych operacjach, niezwykle ważna jest jednak asynchroniczna natura Node. Dowiesz się jak działa, a także jakie są jej zalety. Na tym etapie nie zabraknie oczywiście praktycznego projektu, którym będzie aplikacja typu CLI (Command Line Interface). Pozwoli nam ona grupowo zmienić nazwy wielu plików, według podanego wzorca.
W kolejnym rozdziale poruszymy fundamentalną dla Node kwestię, mianowicie tworzenie serwerów sieciowych. Zaczniemy od najniższego poziomu, tworząc prosty serwer bazujący na protokole TCP/IP. Wiedza w jaki sposób Node radzi sobie z takim zadaniem, pozwoli później lepiej zrozumieć wyższe warstwy, takie jak np. HTTP czy WebSockets. Chwilę później stworzymy swój pierwszy serwer HTTP, do którego będzie można wysyłać zapytania wprost z przeglądarki internetowej. Omówimy również jak w Node wykorzystać bezpieczne szyfrowanie HTTPS.Na tym etapie będziesz już miał solidną wiedzę jak pracować z Node, jednak zanim przejdziemy dalej, zatrzymamy się, by omówić jak dokładnie działa Node.js, a także jak ta platforma została zbudowana. Dwie dość obszerne lekcje, będą być może jednymi z najważniejszych w tym kursie. Dowiesz się w nich czym jest, a także jak działa jednowątkowa pętla zdarzeń, czym jest proces i wątek, a także zobaczysz kod źródłowy Node.js i wszystkie bloki budulcowe, z jakich Node się składa. To naszym zdaniem niezwykle ważny temat, często jednak pomijany w wielu materiałach. Dzięki dogłębnemu zrozumieniu jednowątkowej natury języka JavaScript i asynchroniczności Node, będziesz mógł tworzyć dużo lepsze aplikacje.
Node.js posiada kilkanaście wbudowanych modułów, z których w dowolnej chwili można skorzystać. Wszystkie jednak dają nam dużą, niskopoziomową kontrolę, ale czasami sporo kodu należy napisać, by zrealizować jakieś zadanie. Jest to jednak świadomy wybór twórców Node, gdyż całą resztę świetnych rozwiązań, dopisuje nieustannie ogólnoświatowa społeczność deweloperów. Zdecydowana większość z modułów ląduje w rejestrze npm. Jest to genialny package manager, który pozwoli nam wyszukiwać, pobierać i aktualizować niezliczoną ilość modułów. Dowiesz się zatem jak korzystać z npm, a chwilę później zaczniemy wykorzystywać zgromadzone tam moduły w dalszej pracy.Jednym z najpopularniejszych modułów jest express.js, który pozwala w bardzo prosty sposób tworzyć serwery HTTP. Grzechem byłoby zatem z niego nie skorzystać. Dowiesz się wszystkiego co niezbędne, by zacząć tworzyć własne aplikacje bazujące na protokole HTTP z użyciem express.js, a także kilku dodatków do tego systemu. Zobaczysz jak routować zapytania, jak korzystać z middleware czy serwować statyczne pliki. Następnie omówimy współpracę z bazą danych MongoDB i z jej wykorzystaniem stworzymy API typu REST. Stworzona aplikacja pozwoli odczytywać dane z bazy i przesyłać je do klienta w formacie JSON, a także dodawać nowe dane, aktualizować i usuwać je. Wszystko to zgodnie z dobrymi praktykami pracy z REST.
Chwilę później czeka Cię kolejny praktyczny projekt - skracacz linków. Stworzymy wspólnie aplikację, która pozwoli skrócić dowolny adres URL do krótkiej formy. Oczywiście będzie działać również w drugą stronę! Kiedy przejdziemy pod skrócony adres, przekieruje nas do odpowiedniej witryny. Node.js to platforma wręcz stworzona do tworzenia aplikacji typu real-time, dlatego w kolejnym rodziale podejmiemy temat technologii WebSockets. Dowiesz się jakie są jej zalety, a także jak pracować z nią po stronie serwera i klienta. Tutaj czeka nas kolejny praktyczny, niezwykle ciekawy projekt. Wykonamy bowiem czat grupowy. Aplikacja ta pozwoli dowolnej ilości użytkowników podłączyć się do czatu podając swój nick, a następnie wysyłać widoczne dla wszystkich wiadomości. Zaimplementujemy nawet takie rozwiązania jak wyświetlanie statusów o dołączeniu kogoś do czatu, a także o jego opuszczeniu.W przedostatnim rodziale tego kursu podejmiemy tematykę dobrych praktyk pracy z Node.js. Na początku omówimy najważniejsze konstrukcje nowej specyfikacji EcmaScript 2015, które są znakomicie wspierane w Node. Chwilę później omówimy jak korzystanie z Promises uprości, a także ulepszy nasz kod. Wśród dobrych praktyk nie zabraknie również informacji o debugowaniu aplikacji. Zobaczysz sprawdzone sposoby, by znaleźć błędy lub lepiej, krok po kroku, zrozumieć jak działa napisany wcześniej kod. Dowiesz się również jak pracować z błędami, by Twoje aplikacje działały w sposób przewidywalny.
Ostatni rozdział w całości został poświęcony temu, co zwykle jest pomijane w innych materiałach, mianowicie wdrażaniu aplikacji do produkcji. Przez cały ten kurs pracować będziemy lokalnie, lecz kiedy aplikacje są już gotowe, wypadałoby udostępnić je światu. Wdrażanie aplikacji napisanych z użyciem Node.js nie jest jednak tak oczywiste, jak np. wgrywanie WordPress’a u wybranego hostingodawcy. Ty będziesz miał jednak możliwość zobaczyć, jak wdrożyć napisany przez nas grupowy czat na platformie Heroku. Jest to bardzo popularny serwis działający jako PaaS (Platform as a Service). Za darmo będziesz mógł w ciągu kilku chwil uruchomić swoją aplikację.Zobaczysz o co należy zadbać, by wszystko poszło gładko. Serwisy typu PaaS dbają o bardzo wiele aspektów wdrażania i serwowania naszych aplikacji, takich jak bezpieczeństwo oraz nieustanną dostępność. Mają jednak pewne ograniczenia. Z tego powodu, dowiesz się również jak wdrożyć swoją aplikację na serwerze wirtualnym VPS z systemem Ubuntu Server. Takie rozwiązanie daje nam całkowitą kontrolę, ale także obarczone jest większą odpowiedzialnością. Zaczniemy od instalacji na serwerze platformy Node.js, systemu kontroli wersji GIT, a także innych niezbędnych modułów
Następnie zobaczysz jak skonfigurować swoje lokalne środowisko tak, by za pomocą GIT’a wysyłać kod do zdalnego serwera, a potem jednym poleceniem wdrażać go do produkcji. Dowiesz się również jak jednocześnie serwować wersję produkcyjną oraz developmencką. W rodziale tym poruszymy również inne kwestie, takie jak procesy potomne czy tworzenie klastrów. Dzięki tej wiedzy, będziesz mógł maksymalnie wykorzystać dostępne zasoby serwera. Na sam koniec tego kursu rzucimy okiem również na inne, nie przedstawione wcześniej zastosowania Node, a także nakreślimy dalszą drogę nauki w tym zakresie.
Kurs ten jest dla wszystkich osób, które dobrze czują się w technologiach frontendowych, tj. HTML, CSS i JavaScript, a teraz chcą rozpocząć swoją przygodę z back-endem. Im zatem lepiej znasz język JavaScript, tym więcej wyciśniesz z Node, natomiast nie jest wymagana bardzo zaawansowana wiedza z zakresu tego języka.
W tej lekcji jeszcze chciałbym dosłownie kilka słów powiedzieć na temat obsługi błędów i jak bardzo
jest to ważne i dlaczego zawsze powinieneś szczególną uwagę na to zwracać.
Stworzyłem sobie taki prosty moduł readFile, w którym na końcu eksportuję dwie metody tym zapisem ECMAScript 6,
żebym nie musiał pisać readFile: readFile,
to zrobiłem uproszczoną wersję i tutaj sobie to będę chciał zaimportować. Czyli readFile oraz
readFileSync.
Nic skomplikowanego.
I teraz chcę z jednej z tych metod skorzystać, przekazując jej tutaj ścieżkę do pliku lorem1, który
znajduje się tutaj w katalogu files. Czyli zostanie wykonana metoda czy funkcja readFile, którą mam dokładnie
tutaj.
Ona otrzyma ścieżkę i zauważ, że w środku tworzę sobie EventEmittera, a to pokazywałem ci na początku
tego kursu, i standardowo odczytuję ten plik.
Jeżeli będziemy mieli błąd, to na tym EventEmitterze chcę zrobić zdarzenie error i przekazać tam obiekt
błędu.
A jeżeli uda się dane odczytać, czyli nie będzie błędu, to robię zdarzenie data.
Te zdarzenia można w zasadzie dowolnie nazywać.
Natomiast na końcu sobie tego EventEmittera zwracam.
Czyli wygląda to bardzo podobnie jak promise, z której korzystaliśmy wcześniej. I teraz skoro go zwracam
w tej funkcji readFile, to znaczy, że ona się szybko wywoła.
Te pliki jeszcze nie zdążyły zostać otwarte, bo to zostało oddelegowane do tych API C++.
No i mamy tutaj pod file tego EventEmittera, dlatego mogę do niego przypisywać zdarzenia i przypisałem sobie na data
i na error
normalnie, standardowo. Więc wpiszę teraz node index.
Udało się to wszystko wywołać i nie ma najmniejszego problemu.
Natomiast dlaczego pokazałem ci, że korzystamy tutaj z EventEmittera? Zrobimy sobie tutaj teraz lorem4.
Jest to plik, który nie istnieje, więc tutaj pojawi się błąd i zrobimy
ev.emit("error"). U nas zostanie to przechwycone, czyli ten console.log powinien się wykonać. I nie ma problemu.
Natomiast zauważ, co by się stało -
jest to specjalnie ważne w przypadku EventEmittera.
Nie jest to automatycznie robione przez JavaScript, tylko twórcy Node w ten sposób to stworzyli.
Jeżeli jakikolwiek obiekt, który jest EventEmitterem albo z niego dziedziczy, a w Node.js cała masa
takich obiektów jest i cała masa w internecie modułów, które z tego korzystają.
Jeżeli taki obiekt w dowolnym swoim miejscu zrobi emit("error"), a powiedziałem ci, że te nazwy są dowolne,
ale kilka z nich jest specjalnych, m.in. error. Czyli jeżeli zrobi emit("error"), a nigdzie nie przypiszemy obsługi,
czyli on error, to zobacz, co się stanie.
Otóż mamy tutaj problem: throw error i nasz proces się zakończył.
Czyli gdybyśmy byli na serwerze, który nasłuchuje na połączenia, a stałoby się coś takiego, to cały nasz
serwer upadnie i nikt do niego nie będzie mógł się podłączyć.
Czyli jest to taka specjalna sytuacja w EventEmitterze, że jeżeli on robi emit("error"), a nie zostało przypisane
on error, to specjalnie jest on poprzez throw wyrzucany.
Więc warto o tym wiedzieć.
Teraz druga rzecz, którą chciałbym zrobić, to funkcja
readFileSync.
I ona na razie działa tak, że odczytuje jakiś plik synchronicznie
i tutaj poprzez return go zwraca. Czyli będziemy chcieli sobie ją wywołać gdzieś niżej.
To może wykomentuję, w zasadzie całość.
Zauważ, co tutaj robię. Korzystam z mojej metody readFileSync, przekazuję tam ścieżkę do pliku - zróbmy sobie plik,
który istnieje - i następnie wyświetlam sobie jego treść. Jest to buffer.
Nie ma problemu. Mógłbym tutaj zrobić toString, czyli to tylko tutaj zwróciłem.
Ale teraz co jeśli pojawiłby mi się w tym miejscu błąd, czyli przekażę ścieżkę, która nie istnieje?
No to będziemy mieli problem.
I teraz jeżeli ty będziesz tworzył takie synchroniczne funkcje niekoniecznie odczytujące pliki, ale wykonujące
jakiś kod JavaScript, to zawsze korzystaj z bloku try catch. Już o tym wcześniej nawet wspominałem.
Zróbmy try, czyli spróbujemy to wyrzucić. I catch,
czyli jeżeli tutaj będzie błąd, bo nie uda się otworzyć takiego pliku, to nie chcę zwracać tego błędu,
ale zwrócę sobie null.
I zauważ, że takim prostym zapisem nasza funkcja readFileSync teraz działa tak, że jeżeli uda się odczytać
plik, to zwróci jego dane, a jeżeli się nie uda, to zwróci null
I teraz pod file2 powinniśmy mieć null i nie mieć żadnego problemu.
Więc to jest świetne, że możemy takim prostym zapisem to zrobić.
Ale warto o tym zawsze pomyśleć i o to zadbać. W ten sposób możemy komuś udostępnić funkcję, która na
pewno nie wygeneruje błędu.
I teraz ostatnia rzecz, którą chciałbym ci pokazać, dotyczy process.nextTick. Przejdziemy sobie w to miejsce raz
jeszcze. Odkomentuję to i pokażę ci, co możemy zrobić w tym miejscu. Mianowicie stworzyłem funkcję readFile,
która przyjmuje ścieżkę. Wypadałoby sprawdzić, czy ktoś taką ścieżkę podał.
Co jeżeli jej nie podał?
Zróbmy sobie to w tym miejscu:
if(!path), czyli ścieżka nie została podana,
to wydawałoby się, że żaden problem, chcemy zakończyć funkcję poprzez return.
No i możemy tu od razu zrobić EventEmittera, bo już go mamy - emit(err)
i tutaj sobie napiszemy new error.
Znaczy tutaj powinien być string a tutaj new Error. I napiszemy sobie: path musi być podane.
Wydawałoby się - nic prostszego, skoro w tym miejscu nasłuchujemy na error, to on zostanie tutaj przekazany
i wszystko będzie w porządku.
No to sprawdźmy, czy rzeczywiście tak będzie. Zakończy się ta funkcja.
No i zauważ, że mamy błąd, ale to nie stąd, dlatego że path było podane, więc musimy teraz zrobić tak, żeby path nie
podawać. Więc ja to sobie wykomentuję w tym miejscu, bo jest to ten parametr w tym miejscu przekazywany.
Teraz nie będzie podany, czyli nasza instrukcja się wykona.
Jeszcze raz to sprawdźmy.
I zauważ, że mamy błąd, a nie powinniśmy go mieć -
unhandled error. Skąd on się wziął?
Mianowicie mówiłem ci, że jeżeli nie przypiszemy do takiego EventEmittera on error, tak jak to zrobiliśmy
tutaj, a gdzieś zrobimy ev.emit("error"), to zostanie on wypluty przez throw.
No ale mógłbyś pomyśleć, że przecież my przypisaliśmy tutaj taką obsługę.
Ale zauważ, w którym miejscu. Tutaj robimy od razu return ev.emit("error"), a dopiero na końcu robiliśmy return.
To co widzisz w tym miejscu -
ten EventEmitter. Dopiero wtedy, kiedy zrobiliśmy tutaj return, to dopiero później do niego przypisaliśmy takie
zdarzenie obsługi error.
Jeśli zatem tutaj będziemy robić ev.emit("error"), nawet nie musielibyśmy mieć tego return,
ale funkcję jakoś trzeba w tym momencie zatrzymać, to wykonujemy go na obiekcie, który jeszcze sobie obsługi
nie przypisał.
Z tego właśnie powodu będziemy tutaj musieli skorzystać z process.nextTick.
W ten sposób to wygląda.
I tutaj podaje się funkcję, która ma się wykonać w następnej iteracji tej pętli zdarzeń, o której w tym kursie
już mówiliśmy.
I mam nadzieję, że wiesz, jak ona działa.
Jeżeli to wpiszę w ten sposób, to tego problemu już nie będzie.
Zauważ - teraz błąd się wyświetlił, ale jest to błąd wyłącznie z tego console.loga, który zrobiliśmy tutaj.
Nie jest to już ten sam błąd. Więc prześledźmy, jak to działa.
Otóż interpreter JavaScript interpretuje sobie cały ten plik i widzi, że mamy tutaj EventEmittera, stworzyliśmy
go. Widzi, że mamy tutaj sprawdzenie, czy path zostało podane. Nie zostało podane.
Więc mówimy: w następnej iteracji tej pętli, kiedy ona już będzie wolna,
kiedy się obróci, to wykonaj ten kod. Czyli ta funkcja jest odłożona do kolejki i nic z nią nie robimy.
Przechodzimy sobie dalej.
No ale tutaj jeszcze wypadałoby zrobić return ev, czyli zwrócić sobie ten właśnie EventEmitter, żebyśmy już
tutaj nie przeszli.
Więc raz jeszcze to wykonajmy. Ok. Teraz trochę inaczej ten błąd wygląda.
Przed chwilą się pomyliłem, więc raz jeszcze. Przechodzimy do path. Widzimy, że nie jest podane, mówimy: w
następnej iteracji pętli zdarzeń wykonaj tę funkcję. Odłożyliśmy ją do kolejki i spadamy niżej. Zakończ
tę funkcję, zwracając EventEmittera. Zakończył ją, no ale nie zakończył wykonywania jeszcze wszystkiego,
co ma do wykonania, więc przeszliśmy tutaj i jesteśmy w tym miejscu.
To zostało zwrócone.
Tutaj do tego EventEmittera przypisujemy data oraz error.
Później robimy console.log i dopiero w tym miejscu pętla zdarzeń może się obrócić, bo nie ma już nic
innego do wykonania i z kolejki pobrać to, co zostało tam wystawione. A w kolejce jest ta funkcja i ona
się wykonuje. I dopiero wtedy wszystko działa poprawnie, bo do tego EventEmittera w tym miejscu zostało
już przypisane zdarzenie error.
Dlatego dopiero teraz działa to poprawnie.
Pamiętaj zatem, aby zawsze korzystać w takich sytuacjach z process.nextTick. Ale być może ja tę sytuację
tutaj trochę zagmatwałem tym całym EventEmitterem.
Więc wyobraźmy sobie sytuację, że to wszystko byłoby dużo prostsze i na sekundę wykomentuję tę funkcję
readFile,
a stworzymy tutaj dużo prostszą funkcję, taką jak już w tym kursie mieliśmy.
Ona będzie przyjmować path i callback do wykonania i szybko tutaj to wkleję.
Pamiętasz, że taki przykład mieliśmy, czyli ktoś nam przekazuje ścieżkę i callback do wykonania.
Teraz ją eksportujemy, więc po drugiej stronie z niej skorzystamy.
To sobie wykomentuję i skorzystamy z takiej właśnie prostej funkcji, czyli readFile, ścieżkę podamy sobie taką
samą jak tutaj, tylko zmienimy na lorem1, tak żeby ten plik istniał.
A jako drugi parametr podajemy ten callback nasz własny z error i z data.
I dopiero tutaj mogę sobie coś z tym zrobić. Jeżeli error, to zrobię console.log.
W przeciwnym wypadku wyświetlimy sobie dane jako buffer. To już wcześniej mieliśmy, dlatego dokładnie nie tłumaczę.
Podajemy ścieżkę i podajemy callback. Czyli cała ta funkcja będzie dostępna tutaj, pod cb.
Zauważ, że to jest proste, nie mamy tutaj żadnego tak jak wcześniej EventEmittera. I wydaje się, że jest
wszystko w porządku, bo ta funkcja będzie asynchroniczna.
Ale gdybym w tym miejscu sprawdził, czy ktoś podał path, jeżeli np. (!path), to wykonałbym jego callback z błędem.
Błąd utworzymy sobie sami.
Path musi być podane.
To wydaje się, że nie ma żadnego problemu. Jeżeli ktoś poda to path, buffer się zwrócił.
Ale jeżeli nie poda, czyli wpisze np. wartość null,
to oznaczałoby, że ktoś nie podał poprawnej ścieżki, bo w tym przypadku nikt nie będzie tutaj wpisywał null,
ale chodzi mi o sytuację, w której byśmy zwalidowali, że np. nie jest to poprawna ścieżka albo cokolwiek.
Byłaby tu jakaś realizacja ale synchroniczna, bo ta funkcja się wywołuje. Wpadamy od razu w to miejsce i
zauważ, że nic tutaj nie ma asynchronicznego. Sprawdziliśmy jakiś warunek i synchronicznie jakby wykonujemy
od razu ten jego callback, przekazując mu error.
Zauważ, że to bez problemu będzie działać.
Nie ma żadnego problemu.
Tutaj akurat pojawił się kolejny błąd, bo nie zrobiłem tutaj return z pośpiechu. Więc przeszliśmy dalej.
Zrobię to raz jeszcze. Teraz jest wszystko w porządku.
Czyli wystąpił błąd: Path musi być podane.
I wydawało by ci się, że nie ma problemu. Zrobiliśmy świetną funkcję, ale to nieprawda, dlatego
że my komuś udostępniając taki moduł, mówimy, że jest to moduł asynchroniczny. Czyli podajesz tutaj callback, który
się wykona po jakimś czasie. A on tutaj wykonał się tak naprawdę synchronicznie
w przypadku takiego błędu. Czyli jeżeli ktoś by tutaj miał console.log Hello!, no to zauważmy, kiedy on się wykona.
Najpierw wykonało się: wystąpił błąd, a później Hello!
Natomiast gdyby nie było błędu, gdyby ścieżka była poprawnie podana, powiemy, że to wykona się dopiero
później i ten callback
i wtedy byłoby na odwrót, czyli najpierw wykonałoby się Hello!, a potem wystąpiłby błąd.
Nie powinniśmy tego robić w ten sposób, skoro komuś dajemy taką funkcję, gdzie podaję callback, to on
zakłada, że ona zawsze jest asynchroniczna
Dlatego nawet jeżeli będziemy mieli tutaj wywołanie takie synchroniczne, czyli będzie od razu jakiś błąd,
to ten błąd należy też umieścić w process.nextTick.
I teraz jeżeli zrobisz to w ten sposób, zauważ, jak zmieni się nam tutaj console.log
Najpierw będzie Hello!, a potem wystąpił błąd. Więc mam nadzieję, że teraz już dobrze to rozumiesz. Bo najpierw
troszkę to zagmatwałem w przypadku tego EventEmittera, bo chciałem powiedzieć, co dzieje się, kiedy nie piszemy
zdarzenia on error.
Niemniej jednak widzisz teraz, jak działa process.nextTick.
Nawet jeżeli coś jest tutaj synchronicznie, ale z natury nasza funkcja powinna być asynchroniczna
dla kogoś, to powinniśmy z takiego process.nextTick korzystać.