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.
Mam nadzieję, że do tej pory całkiem nieźle poznałeś już Node.js.
Natomiast w tej i w kolejnej lekcji będę chciał ci powiedzieć, jak tak naprawdę działa Node.js
pod spodem.
Według mnie te dwie lekcje są być może najważniejsze w całym tym kursie.
Natomiast jeżeli nie wszystko dobrze w nich zrozumiesz, to koniecznie wróć do nich
może później, po obejrzeniu całego tego kursu. Abyśmy mogli odpowiedzieć sobie na pytanie, jak działa Node,
to musimy poznać najpierw kilka pojęć.
Przede wszystkim musisz rozumieć, czym jest proces i czym jest wątek. Więc nie przedłużając, proces jest
to instancja uruchomionego programu, której system operacyjny przydzielił zasoby takie jak czas procesora,
pamięć operacyjną, dostęp do urządzeń wejścia/wyjścia czy pliki. Każdy nowo uruchomiony proces posiada
własną, niezależną przestrzeń adresową.
Co to oznacza w praktyce?
Otóż jeżeli uruchomisz sobie program Photoshop, to mamy instancję takiego programu, czyli proces Photoshop.
Jeżeli uruchomimy sobie w konsoli - tak jak wpisywaliśmy node - takie polecenie, to uruchamiamy proces Node i każdy
z tych procesów jest zarządzany przez system operacyjny.
Oznacza to tylko tyle, że system przydziela odpowiednią ilość pamięci RAM, czas pracy procesora itd dla
każdego procesu i każdy z nich ma niezależną przestrzeń adresową.
Czyli jeżeli my tworzyliśmy sobie w Node jakieś zmienne, to wiemy, że one w pamięci RAM zostaną zapisane
i nie martwimy się o to, że kiedy my stworzyliśmy zmienną, to być może wystawimy ją do pamięci w miejscu,
gdzie jest jakiś otwarty plik Photoshopa.
Nie jest to możliwe, dlatego że każdy proces ma niezależną przestrzeń adresową.
Teraz czym jest wątek (z angielskiego thread)?
Mianowicie jest to część programu wykonywana współbieżnie i uruchamiana przez proces. Proces może sobie wiele
wątków uruchomić. I wątek tym różni się od procesu, że współdzieli z innymi wątkami zasoby przydzielone
dla takiego procesu. Czyli przed sekundą powiedziałem, że procesy mają niezależną przestrzeń adresową
i zasoby ale wątki już nie.
Czyli proces może sobie wewnątrz siebie uruchomić wiele takich wątków i każdy z nich będzie współdzielił te zasoby,
które są przydzielone dla procesu.
Wątki są świetne dlatego, że one wymagają mniej zasobów do działania i są dużo szybciej tworzone niż
procesy.
Z uwagi na to, że współdzielą zasoby, to mogą również między sobą przekazywać dowolną ilość danych
bez ich kopiowania, tak jak ma to miejsce między procesami, ale mogą jedynie przekazać sobie wskaźniki
do pamięci operacyjnej. I teraz mógłbyś zapytać, jak się tworzy wątki?
Otóż robi się to już za pomocą języków programowania. Kiedy programujemy, to możemy skorzystać z wątków.
Jest to takie programowanie wielowątkowe.
Natomiast co nam dają wątki?
Otóż wrażenie współbieżności, bo procesor bardzo szybko przełącza się pomiędzy wątkami i wykonuje raz
kod z jednego wątku, raz drugiego, a nam wydaje się, że to wszystko dzieje się jednocześnie.
Jeżeli masz komputer, w którym jest procesor wielordzeniowy i wielowątkowy, to faktycznie może być jednocześnie
wykonywanych wiele czynności.
Natomiast jeszcze kilka lat temu mieliśmy procesory, które były jednordzeniowe, jednowątkowe, a mimo tego mogliśmy
oglądać film na YouTube, pisać z kimś na czacie i jednocześnie jeszcze w tle wykonywała się masa innych
operacji, które robił system operacyjny i nam wydawało się, że wszystkie te rzeczy dzieją się równolegle, ale
wcale tak nie było.
To system operacyjny pomaga procesowi przełączać się szybko pomiędzy poszczególnymi zadaniami i raz
powiedzmy dekodujemy sobie fragment filmu, który jest pobierany z YouTube'a i wyświetlany, a za chwilkę - i mam
na myśli naprawdę niewielką chwilkę, to są bardzo małe odstępy czasowe - wykonujemy kod jakiegoś innego
programu, np. Photoshopa.
Dla użytkownika wydaje się, że to wszystko dzieje się równolegle.
Teraz żeby zrozumieć, jak działa Node, to najpierw zatrzymamy się w przypadku języka PHP i serwera Apache, z którym
jest duża szansa, że miałeś już do czynienia.
Jeżeli kiedykolwiek na jakimś hostingu postawiłeś sobie WordPressa, to na 99% korzystałeś z serwera
Apache,
no i z języka PHP, w którym WordPress jest napisany.
No i teraz jak to działa?
Otóż jeżeli ktoś wchodzi na naszą stronę, to dla każdego połączenia HTTP jest tworzony nowy wątek lub
jest przydzielony już wcześniej utworzony wątek, który oczekuje w tzw. puli wątków. Czyli serwer Apache
otrzymuje połączenie, ktoś chce wyświetlić nasz wpis na blogu i przydziela do tego połączenia
wątek. Taki wątek obsłuży żądanie, czyli powiedzmy wykona cały ten kod PHP. W tym kodzie są odwołania do
bazy danych, z bazy danych zwrócimy te wpisy, połączymy to z kodem HTML i odeślemy do klienta. W tym momencie
wątek wróci w stan oczekiwania
w tej puli wątków i może za moment być przydzielony do kolejnego połączenia.
Minusem takiego rozwiązania jest fakt, że zasoby,
tak jak wspomniałem, przydzielone przez system operacyjny dla każdego wątku mogą się marnować w momencie,
gdy wykonujemy synchroniczne operacje, takie jak np. odczytywanie czegoś z bazy danych czy otwieranie pliku.
Aby to lepiej zobrazować, to przejdziemy sobie do takiej instrukcji w PHP, którą być może kiedyś widziałeś.
Tworzymy sobie zmienną content i za pomocą funkcji file_get_contents mówimy: Odczytaj ten plik file.txt.
Kiedy to zrobisz, to dopiero dalej przejdziemy.
Czyli jest to bardzo podobne do funkcji readFileSync, którą ci pokazałem w Node.js.
Nie możemy przejść do kolejnej linijki, dopóki z dysku nie zostanie ten plik odczytany.
Czyli wspomniałem ci, że system operacyjny dla takiego wątku przydzieli zasoby, np. pamięć RAM czy czas
pracy procesora, co jest dla nas najważniejsze, a my w tym momencie nie korzystamy prawie w ogóle z tego
procesora.
Bo powiedzieliśmy: Otwórz plik i czekamy, aż dysk twardy nam ten plik odczyta.
W tym momencie te zasoby przydzielone dla tego wątku się marnują.
Podobnie w Node.js gdybyśmy korzystali z funkcji synchronicznych, byłoby tak samo.
Ale mamy funkcje asynchroniczne, o których już mówiliśmy. I zaraz jeszcze zobaczysz, jak to wszystko działa.
Przejdźmy zatem do tematu Node.js.
Możesz spotkać się w internecie z takim określeniem, że Node.js jest jednowątkowy.
Nie jest to prawdą.
Jako platforma jest wielowątkowy, natomiast posiada jednowątkową pętlę zdarzeń. I żeby to lepiej zrozumieć,
to zobaczmy, z czego złożony jest sam Node.js. Mianowicie z silnika V8 od Google.
Jest to interpreter języka JavaScript i dodatkowo z bibliotek C++.
I to wszystko razem tworzy nam Node.
Do tego dochodzą jeszcze moduły napisane w JavaScript, ale o tym powiemy sobie w lekcji kolejnej, teraz nie
jest to istotne.
I teraz sam silnik V8 jest jednowątkowy. Czyli JavaScript w przeglądarkach, bo w przeglądarce np. Chrome też
mamy V8, zawsze jest wykonywany w jednym wątku. Natomiast sam Node.js w tej całości nie jest już jednowątkowy,
jest wielowątkowy.
Czyli wróćmy do przykładu, o którym mówiłem, że mamy komputer z procesorem jednordzeniowym i jednowątkowym.
On nie może wykonywać wielu czynności jednocześnie.
Więc wyobraźmy sobie sytuację, że tutaj w Node odczytujemy jakiś plik, tak jak ci do tej pory pokazywałem, np.
funkcją readFile.
Pamiętasz, że tam podaliśmy callback, który miał się wykonać dopiero wtedy, kiedy zostanie odczytany taki plik.
A my mogliśmy od razu wyskoczyć z tej funkcji i kontynuować jej wykonywanie.
Dokładnie tak będzie nawet na komputerze, który ma jednordzeniowy procesor. Dlatego, że będziemy się przełączać
pomiędzy wątkami.
Czyli w praktyce nie będzie momentu, w którym dwie rzeczy są wykonywane jednocześnie, ale procesor tak
szybko będzie się przełączał pomiędzy odczytywaniem tego pliku tutaj w C++, a pomiędzy interpretowaniem
kolejnego kodu JavaScript, że będzie się wydawać, że dzieje się to równolegle.
Aby jeszcze lepiej to zrozumieć, przejdziemy do kolejnego przykładu i pokażę ci, jak działa pętla zdarzeń.
Po prawej stronie mamy proste kody JavaScript.
Natomiast ważne jest to, co mamy później, czyli stos (z angielskiego stack). Wyobraź sobie, że to jest jakby ten
nasz silnik V8, który będzie interpretował JavaScript.
To, co tutaj zobaczysz, jest wykonywane jednowątkowo,
czyli jedna rzecz w danym momencie, nie może to być wykonywane współbieżnie. Natomiast obok mamy Node.js APIs
i to są te API, które na poprzednim slajdzie pokazałem jako C++.
Jak zatem interpreter JavaScriptu V8 zacznie wykonywanie takiego kodu? W uproszczeniu będzie to wyglądać
tak, że weźmie sobie pierwszą funkcję console.log i wstawi ją na stos. Wykona ją i jak widzisz, u dołu pojawiło
nam się Hello.
Ściągniemy ją sobie ze stosu i teraz przejdziemy do wykonywania kolejnej funkcji. Natomiast setTimeout
jest funkcją, która w samym JavaScript nie istnieje i w przeglądarkach jest wykonywana przez specjalne
API przeglądarkowe,
a w Node.js przez to API C++.
Niemniej jednak twórcy Node dopisali do V8 coś takiego.
Mówiąc mu: Jeżeli spotkasz funkcję setTimeout, wiemy, że nie potrafisz tego wykonać w JavaScript, ale w
tej funkcji znajdziesz inną funkcję też napisaną w JavaScript.
Możesz nam to oddelegować tutaj, do takiej specjalnej funkcji w C++.
I nie martw się, przejdź sobie do następnej linijki kodu i sobie ją interpretuj.
My natomiast kiedy wywołamy sobie taki timer, to tę funkcję callback ci zwrócimy i będziesz mógł sobie ją wykonać, bo przecież
ona jest napisana w języku JavaScript, który doskonale rozumiesz.
No więc przejdziemy sobie do tego setTimeout.
Mamy to na stosie. Silnik V8 wykonał to, co wykonać potrafił, czyli oddelegował sobie to do Node.js APIs
i ściągnął ze stosu.
Praca dla tego jednego wątku
w tym momencie się zakończyła. A to, co dzieje się tam po stronie C++, to powiedzmy, że tego głównego
wątku nie interesuje. On o tym zapomniał.
Przechodzi sobie zatem do kolejnej linijki, wykonuje world, ściąga sobie to ze stosu i w konsoli mamy
Hello world!
Mijają jednak trzy sekundy i ten timer, który był po stronie C++ uruchomiony, zwraca nam ten callback. Ale on
nie upcha nam go na stosie, bo na stosie mogłyby się dziać inne rzeczy i w środek naszego kodu nie może
nam tego wstawić.
Dlatego on nam to wrzuci do kolejki, którą widzisz u dołu. I teraz zadziała magiczna pętla zdarzeń, czyli
jeżeli nie mamy na stosie żadnego kodu, który aktualnie jest wykonywany, to pętla zdarzeń będzie się obracać
i sprawdzać, czy jest coś w kolejce.
Zatem obróci się, sprawdzi, że był tam callback, wrzuci nam go na stos.
No i znowu interpreter JavaScript
skoro ma coś na stosie, to to wykona, wykona funkcję callback z console.log
3 sekundy później, czyli to, co widzimy tutaj, ściągnie to ze stosu
i w tym momencie proces się zakończy, dlatego że nie oczekujemy już na żadne inne eventy czy nie oddelegowaliśmy
żadnego setTimeout albo otwierania jakiegoś pliku.
Mam nadzieję, że na tym etapie jest to zrozumiałe, ale za moment zobaczysz to jeszcze w kodzie i wtedy
jakby na własne oczy zobaczysz, o co chodzi.
Na tym etapie zapamiętaj, że w miejscu funkcji setTimeout równie dobrze moglibyśmy mieć otwieranie
jakiegoś pliku. Gdyby to otwieranie było synchroniczne, to nie możemy wykonać linijki console.log world, póki
nie otworzymy takiego pliku.
Ale gdybyśmy to zrobili asynchronicznie, tak jak w tym kursie ci pokazywałem, to dokładnie tak samo V8
wyrzuciłby to do tych API node'owych i interpretowałby kod dalej, a kiedy taki plik zostanie odczytany,
to API node'owe wepchną ten callback do kolejki i pętla zdarzeń się obróci i wstawi to na stos,
aby można to było wykonać.
Nie ukrywam, że ten slajd, który tutaj widzisz, stworzyłem inspirując się prezentacją, którą znajdziesz na
YouTube. Jest genialna, dlatego chcę ci ją polecić, abyś ją obejrzał po obejrzeniu tej lekcji.
Jej twórca - zobacz.
Ang.
No właśnie. Omawia, jak działa pętla zdarzeń, ale podaje tutaj więcej przykładów i dokładniej to tłumaczy.
Pokazuje to na przykładzie przeglądarek internetowych a nie platformy Node, ale jest to niemal identyczne,
dlatego polecam ci obejrzenie tej prezentacji.
Natomiast ja teraz będę chciał pokazać ci w kodzie, że to wszystko, co do tej pory zobaczyłeś
tutaj, tak naprawdę ma swoje odzwierciedlenie w rzeczywistości.
Zauważ, że przygotowałem tutaj przykład identyczny jak przed momentem w prezentacji. I teraz będę chciał,
abyśmy go mogli sobie wywołać i zobaczyć, że dokładnie tak się też stanie. Chociaż to dla ciebie nie będzie
oczywiście zaskoczeniem. Więc wpiszmy sobie node index. I mamy Hello world! w tym miejscu.
A po trzech sekundach pojawi nam się "3 sekundy później".
I znowu działa to tak, jak w tej prezentacji, czyli interpreter V8 wrzuci sobie na stos console.log Hello.
Wykona.
Wraca tutaj, wykonuje setTimeout i oddelegowuje tę funkcję do API C++.
I to w tych API jest liczone te trzy sekundy.
My natomiast możemy przejść sobie dalej, wykonać world, co stało się tutaj, a po trzech sekundach zostanie
to wepchnięte do kolejki i wstawione z powrotem na stos do wykonania.
Nie ma tutaj nic zadziwiającego.
Ale teraz zaczną się czary, dzięki którym lepiej zrozumiesz, po co nam jest ta magiczna kolejka.
Gdybyśmy mogli odwołać się tutaj do setTimeout i powiedzieć: Wykonaj ten kod za trzy sekundy, ale tutaj
w jakiś inny sposób zatrzymać wykonywanie tego kodu. Czyli w tym miejscu chciałbym wstawić coś, co będzie
albo tak obciążające dla procesora, że potrwa więcej niż trzy sekundy,
albo w jakiś inny sposób wydłuży nam działanie tego procesu. No i stworzymy sobie funkcję sleep, którą
tutaj wkleję. Będzie ona bardzo prosta. Zobacz. Chcę to tej funkcji podać milisekundy i zrobię to tutaj.
Zaraz ci ją wytłumaczę, ale powiedzmy, że w tym miejscu wykonam sobie sleep i powiem, że chcę, aby trwało to sześć sekund.
Wróćmy do tej funkcji, zobaczmy, jak ona działa.
Otóż pobieram sobie tutaj aktualną datę i za pomocą date.now zostanie zwrócona liczba milisekund od pierwszego
stycznia 1970 roku.
I teraz nie wykonuje nic, co jest bardzo obciążające dla procesora, ale wykonuje po prostu zwykłą pętlę
while, która ma się wykonywać - tutaj byłby kod, ale nic tutaj nie wystawiłem - tak długo jak date.now, czyli
za każdym razem sobie pobieram nową datę. Nie będzie przynajmniej o te 6 tysięcy milisekund różna od tego,
co pobrałem wcześniej. Czyli generalnie tutaj będziemy mieli jakąś wartość i później w pętli pobieramy
sobie cały czas nową wartość i mówimy: Tak długo ta pętla ma się wykonywać, aż ta wartość będzie mniejsza
od tych sześciuset czy sześciu tysięcy milisekund, które podałem. W którymś momencie minie te 6 sekund
i będziemy mogli z tej pętli wyskoczyć.
Taka funkcja pozwoli nam zablokować wykonywanie naszego kodu, co możesz zobaczyć w tym miejscu.
Czyli wydawałoby się, że w tym miejscu powiedziałem: Za trzy sekundy wykonaj ten console.log.
Ale to wcale nie oznacza, że on wykona się za trzy sekundy.
Zobaczmy, jak to będzie wyglądać.
Może jeszcze zrobimy jedną ciekawą rzecz, mianowicie skorzystamy z console.time i wpiszemy sobie tutaj setTimeout.
Ten string jest dowolny. Natomiast jeżeli wpiszesz sobie później, zróbmy sobie to może tutaj, console.timeEnd i
podasz ten sam string,
to wyświetli się różnica czasu pomiędzy tym wywołaniem a tym wywołaniem. Czyli zobaczymy, czy to się wykonało
po trzech sekundach czy nie. Wykomentuję sobie ten sleep. Zrobimy node index. Trzy sekundy później nam się
to powinno wykonać.
No i widzisz, że mamy taką oto różnicę. Czyli pamiętaj jedną bardzo ważną rzecz - otóż funkcja setTimeout
nie oznacza, że kod, który tutaj podamy, wykona się po trzech sekundach, ale że wykona się po minimum
trzech sekundach.
W tym przypadku było to 3002 milisekundy. Natomiast teraz jeżeli wstawię sobie tutaj funkcję sleep, która
będzie wykonywać tę pętlę przez 6 sekund, to zobaczmy, jaki będzie efekt.
Czekamy sobie.
Czekamy. I możesz zobaczyć, że mamy tutaj taki console.log
3 sekundy później, ale tak naprawdę wykonało się to 6 sekund później.
W jaki sposób to działa teraz sobie prześledźmy i dzięki temu, mam nadzieję, doskonale zrozumiesz, jak działa
ta magiczna pętla zdarzeń.
Zacznijmy od tej linijki. Czyli interpreter JavaScript wepchnął to sobie na stos.
Wykonał Hello i pojawiło się to w konsoli.
Następnie przeszedł dalej. Tutaj zignorujemy sobie ten console.time, bo to nie jest istotne.
Przeszedł do setTimeout, oddelegował funkcję callback do tych API C++, które ci pokazywałem na prezentacji,
i przeszedł sobie do kolejnej linijki.
W tej linijce zobaczył funkcję, która trwała aż sześć sekund, czyli nie mógł przejść w to miejsce, dlatego
że był zajęty wykonywaniem tego kodu.
A że jest jednowątkowy, no to nie mógł robić dwóch rzeczy jednocześnie.
I choć ta funkcja trwała sześć sekund, to już po trzech sekundach to node'owe API zwróciła nam ten callback i
upchnęło go do kolejki.
Tę kolejkę widziałeś przed momentem na prezentacji. Czyli w tej kolejce siedzi sobie taki oto callback, ale
nie może zostać wykonany tak długo, aż nie zakończy się wykonywanie aktualnych zadań.
No i skoro to aktualne zadanie trwało sześć sekund, to dopiero po sześciu sekundach V8 przeszedł
dalej, ale wcale nie przeszedł do wykonywania tego Timeout, chociaż on czeka w kolejce. Przeszedł do kolejnej
linijki kodu, gdzie miał do wykonania world. Wykonał sobie world i dopiero wtedy stwierdził, że nie ma już nic
do wykonania.
Pętla zdarzeń się obróciła i wyciągnęła z kolejki ten oto callback.
I dopiero wtedy go wykonała.
Jak zatem widzisz, chociaż powiedzieliśmy tutaj: Wykonaj to po trzech sekundach, to po trzech sekundach
było już w kolejce do wykonania, ale tutaj główny wątek był zajęty wykonywaniem czegoś innego.
Mam nadzieję, że dobrze to rozumiesz
na tym etapie. Dlatego teraz pokażę ci taką samą funkcję sleep, ale w przypadku serwera. Będzie to niezwykle
istotne.
I właśnie w kontekście tego, o czym mówimy cały ten kurs, czyli asynchronicznego wywoływania, np. otwierania
plików.
Mamy tutaj serwer, który będę chciał teraz uruchomić za pomocą node server. I serwer działa. Przejdziemy
pod ten adres localhost 8080.
No i mamy Hello world!
Nie ma tutaj nic zaskakującego, już wcześniej to widziałeś.
Niemniej jednak pamiętaj, że to wszystko, co widzisz tutaj, jest jednowątkowe. Czyli ta funkcja jest
wykonywana dla każdego nowego połączenia pod warunkiem, że jakby ten główny wątek nie ma już nic innego
do zrobienia. Jeżeli tutaj byłby zajęty czymś w tej funkcji, to nie mógłby obsłużyć kolejnego połączenia.
I teraz będę chciał ci to pokazać. Ale nie chcę wykonywać tej funkcji sleep tutaj dla każdego połączenia, ale
tylko dla tych, które będą miały taki query string dopisany tutaj pod tytułem kill równa się 1. Więc abyśmy to
mogli sparsować, to tutaj sobie zainkludujemy dwa wbudowane moduły Node.
W zasadzie możemy to zrobić tutaj. Ja je wkleję. Wcześniej ci ich nie pokazywałem.
Znajdziesz je w dokumentacji. Pierwszy z nich jest to url, a drugi to querystring. I one pozwolą nam właśnie
z takiego całego adresu odczytać sobie np. samo to kill równa się 1, a potem jeszcze sparsować tak,
abym mógł napisać sobie odwołanie do obiektu .kill i mieć taką wartość.
Czyli generalnie będzie to wyglądać następująco: url.parse w tym miejscu pozwoli nam przekazać tutaj req.url -
co już ci w tym kursie pokazywałem - i z tego zwróci nam query, czyli dokładnie to, co jest po znaku zapytania. A następnie
to będę chciał przekazać do querystring.parse, czyli do tego drugiego modułu,
a on to sparsuje w ten sposób, że będę mógł później odwołać się do .kill i będę miał wartość 1. Czyli
będę sobie to mógł porównać w ten sposób. Zrobię tutaj na dole trochę miejsca i wrócimy sobie tutaj.
Zapiszę to i ten serwer uruchomimy od nowa.
No i teraz przejdziemy do niego normalnie, czyli nie będziemy mieli tutaj tego kill 1.
Jak widzisz, Hello world! wykonuje się od razu. Mogę odświeżyć.
Nie ma żadnego problemu.
Jeżeli wrócimy sobie do edytora, to możesz zobaczyć, że jeżeli podam kill równa się 1, to wtedy uśpimy sobie poprzez
tę naszą funkcję sleep, czyli tutaj będzie ta główna funkcja, ten główny wątek zajęty na 10 sekund. Abyśmy
to mogli zrobić, to będę chciał sobie otworzyć nową kartę.
I tutaj dodam ?kill=1.
Zauważ, że tutaj musimy czekać.
Nie mamy żadnej odpowiedzi, ale to, co jest niesłychanie ważne, to wydawałoby się, że jeżeli tutaj nie podam
tego kill1, to mogę odświeżyć i otrzymam odpowiedź.
Tak się jednak nie dzieje. Czekamy zarówno tutaj, jak i tutaj. Aby jeszcze lepiej zobaczyć, jak to wygląda,
przejdziemy tutaj do Inspekcji i do panelu Network. Będę chciał odświeżyć.
Tutaj zrobię dokładnie to samo.
Po kilku sekundach - zauważ, że tam już czekamy - odświeżam i zobaczymy, kiedy nam się to wykona. Dopiero po
10 sekundach. Więc mogę teraz kiedy to się nam wczytało, przejść sobie tutaj u dołu, kliknąć kill. Podniosę
to trochę. Przejdziemy do karty Timing.
Ona jest bardzo istotna. I ona nam pokazuje tutaj Waiting (TTFB).
Trwało to aż 10 sekund, zanim otrzymaliśmy odpowiedź serwera.
Zauważ, że tutaj odświeżyłem dopiero po kilku sekundach, więc tutaj jeżeli przejdziemy i przejdziemy do
Timing, to mamy tutaj 5 sekund.
Mam nadzieję jednak, że na tym etapie zaczynasz widzieć, co tutaj się dzieje. Otóż nie wystarczy, że dla
danego połączenia podałem kill. To ono będzie czekać 10 sekund. Tak by się wydawało że tylko wtedy tutaj
będzie uruchomione sleep. Ale przy tym połączeniu nie podałem kill, odświeżyłem i nie mogliśmy dostać odpowiedzi.
Właśnie z tego powodu, że silnik V8 i JavaScript jest jednowątkowy. Czyli skoro miał tutaj robotę w tym głównym
wątku, a tą robotą było wykonywanie tej pętli, to nie mógł przejść do obsługiwania żadnego kolejnego połączenia.
Dokładnie z tego powodu Node.js tak bardzo stawia na asynchroniczność. Bo teraz wkleję tutaj przykład
z plikiem.
Tak to będzie wyglądać. Wykomentuję go, ale nie będziemy uruchamiać, bo nie mam tego pliku w tym katalogu.
Natomiast to sobie na moment wykomentuję.
Zauważ, że tutaj mamy funkcję asynchroniczną. Już w tym kursie ci takie funkcje pokazywałem.
Czyli dla każdego połączenia będziemy wykonywali tę funkcję, a ta funkcja powie: Odczytaj ten plik, ale dopiero,
jak go odczytasz,
czyli to oddelegowujemy do tych API node'owych, to dopiero wtedy odeślij.
Czyli tak naprawdę bardzo szybko to się wykona i skoro w tej funkcji nie ma już nic innego do roboty,
to możemy przyjąć kolejne połączenie.
Natomiast gdybyśmy tutaj odczytywali tak, jak ci pokazywałem synchronicznie, to wtedy mielibyśmy ten problem,
że dopóki by nam ten plik nie został zwrócony, to nie tylko ta funkcja nie mogłaby się zakończyć, ale
żadne inne połączenie nie mogłoby zostać obsłużone dla takiego serwera.
Nie mogłem ci tego pokazać w przypadku sync, bo na naszym komputerze działoby się to tak szybko, że byś tego
nie zauważył.
Dlatego napisałem taką funkcję sleep, która nam tutaj blokowała główny wątek.
Jeśli jednak byśmy tutaj korzystali z takiego odczytywania synchronicznego, a na raz tysiąc użytkowników próbowałoby
się podłączyć do naszej strony, to na pewno zauważylibyśmy, że jest to wąskie gardło naszej aplikacji, bo
nie moglibyśmy kolejnych funkcji przyjmować dla nowych połączeń HTTP, dopóki ten główny wątek nie zostałby
wykonany.
Ja sobie to jednak wykomentuję, bo nam to teraz i tak nie zadziała, bo nie mamy tego pliku w katalogu, a zostawię
to w ten sposób, abyś mógł sobie to na własne oczy przetestować.
Wiem, że być może ta lekcja była skomplikowana, ale z drugiej strony mam nadzieję, że pokazała ci, na czym
polega tak naprawdę asynchroniczność Node i na czym polega to, że główny wątek tutaj JavaScript jest
jednowątkowy, czyli nie możemy wykonywać kilku rzeczy naraz. I w przypadku serwerów HTTP jest to niesłychanie
istotne, aby w tej funkcji obsługi każdego nowego połączenia nie wykonywać zbyt obciążających procesor
czynności lub czynności, na które musimy czekać, a więc np. otwierania plików w sposób synchroniczny.