Spis treści
Część I
Szybki start: instalacja i pojęcia ogólne
19
Rozdział 1
Przygotowanie środowiska Instalacja w systemie Windows Instalacja w systemie OS X Instalacja w systemie Linux
21 22 22 23
Kompilacja Kontrola działania
Narzędzie REPL Node Wykonanie skryptu NPM Instalowanie modułów Definiowanie własnego modułu Instalacja narzędzi binarnych Przeszukiwanie rejestru NPM
Rozdział 2
23 23
23 24 25 25 26 27 28
Podsumowanie
29
Przegląd JavaScript Wstęp Podstawowy JavaScript
31 31 32
Typy Typowa łamigłówka Funkcje Konstrukcje this, call() i apply() Arność funkcji Domknięcia Klasy Dziedziczenie Blok try {} catch {}
JavaScript w wersji v8 Metoda keys() obiektu Metoda isArray() tablicy Metody tablic Metody łańcuchów znaków JSON Metoda bind() funkcji Właściwość name funkcji Właściwość __proto__ i dziedziczenie Metody dostępowe
Podsumowanie
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
32 32 33 34 34 35 35 36 37
38 38 39 39 39 39 40 40 40 41
42
SPIS TREŚCI Rozdział 3
Blokujące i nieblokujące operacje wejścia-wyjścia Duże możliwości to duża odpowiedzialność Blokowanie Jednowątkowy świat Obsługa błędów Ślady stosów wywołań
Rozdział 4
43 44 46 47 50 51
Podsumowanie
53
JavaScript dla Node Obiekt globalny
55 56
Pożyteczne zmienne globalne
System modułów Moduły względne i bezwzględne
56
57 57
Udostępnianie interfejsu programistycznego Zdarzenia Bufory Podsumowanie
59 61 63 64
Część II
Najistotniejsze interfejsy programistyczne Node
65
Rozdział 5
Wiersz poleceń i moduł FS: Twoja pierwsza aplikacja Wymagania Piszemy nasz pierwszy program
67 68 68
6
Tworzymy moduł sync czy async? Zrozumienie strumieni Wejście i wyjście Refaktoring Interakcja z modułem fs
Wiersz poleceń Obiekt argv Katalog roboczy Zmienne środowiskowe Zakańczanie programu Sygnały Sekwencje sterujące ANSI
Moduł fs Strumienie Obserwacja
Rozdział 6
69 70 71 73 75 77
79 79 80 81 81 82 82
82 83 84
Podsumowanie
84
Protokół TCP Czym charakteryzuje się TCP?
87 88
Komunikacja z naciskiem na połączenia i zasada zachowania kolejności Kod bajtowy jako podstawowa reprezentacja Niezawodność Kontrola przepływu Kontrola przeciążeń
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
88 88 89 89 89
SPIS TREŚCI Telnet Czat na bazie TCP Tworzymy moduł Klasa net.Server Odbieranie połączeń Zdarzenie data Stan i monitorowanie połączeń Wykończenie
Klient IRC Tworzymy moduł Interfejs net.Stream Implementacja części protokołu IRC Test z prawdziwym serwerem IRC
Rozdział 7
89 92 92 92 94 96 97 100
102 102 103 103 104
Podsumowanie
104
Protokół HTTP Struktura HTTP Nagłówki Połączenia Prosty serwer WWW
105 106 107 111 112
Tworzymy moduł Wyświetlamy formularz Metody i adresy URL Dane Składamy elementy w całość Dopracowanie szczegółów
Klient Twittera Tworzymy moduł Wysyłanie prostego żądania HTTP Wysłanie danych Pobieranie tweetów
112 112 114 117 119 120
121 121 122 123 124
Moduł superagent na pomoc Przeładowanie serwera za pomocą narzędzia up Podsumowanie
128 130 130
Część III
Tworzenie aplikacji sieciowych
133
Rozdział 8
Framework Connect Prosta strona internetowa przy użyciu modułu http Prosta strona internetowa przy użyciu frameworka Connect Metody pośredniczące
135 136 139 141
Tworzenie metod pośredniczących wielokrotnego użytku Metoda pośrednicząca static Metoda pośrednicząca query Metoda pośrednicząca logger Metoda pośrednicząca bodyParser Ciasteczka Metoda pośrednicząca session
142 146 148 148 150 153 154
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
7
SPIS TREŚCI Sesje Redis Metoda pośrednicząca methodOverride Metoda pośrednicząca basicAuth
Rozdział 9
8
159 160 160
Podsumowanie
162
Framework Express Prosta aplikacja Express
163 164
Tworzymy moduł HTML Konfiguracja Definiowanie tras Moduł search Uruchomienie aplikacji
164 164 165 166 168 169
Ustawienia Mechanizmy szablonów Obsługa błędów Metody złożone Trasy Metody pośredniczące Strategie organizacji Podsumowanie
170 172 173 173 175 177 178 180
Rozdział 10 Technologia WebSocket AJAX Technologia WebSocket Aplikacja Echo Przygotowanie Konfiguracja serwera Konfiguracja klienta Uruchomienie serwera
Kursory myszy
181 182 184 185 185 186 187 188
189
Przygotowanie Konfiguracja serwera Konfiguracja klienta Uruchomienie serwera
189 189 192 194
Kwestie do rozwiązania
194
Zamknięcie połączenia a rozłączenie JSON Ponowne łączenie Rozgłaszanie WebSocket to HTML5: starsze przeglądarki go nie obsługują Rozwiązanie
Podsumowanie Rozdział 11 Framework Socket.IO Transporty Rozłączenie kontra zamknięcie połączenia Zdarzenia Przestrzenie nazw
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
195 195 195 195 195 195
196 197 198 198 198 199
SPIS TREŚCI Czat Przygotowanie programu Konfiguracja serwera Konfiguracja klienta Zdarzenia i rozgłaszanie Gwarancja odbioru
Aplikacja DJ Rozszerzenie czata Integracja z interfejsem Grooveshark Odtwarzanie
Część IV
200 200 200 201 203 207
209 209 210 213
Podsumowanie
218
Bazy danych
219
Rozdział 12 MongoDB Instalacja Dostęp do MongoDB: przykład uwierzytelnienia użytkownika Konfiguracja aplikacji Tworzymy aplikację Express Łączymy się z MongoDB Tworzymy dokumenty Wyszukiwanie dokumentów Metoda pośrednicząca do uwierzytelniania Sprawdzanie poprawności danych Niepodzielność Tryb bezpieczny
Wprowadzenie do Mongoose Definiowanie modelu Definiowanie zagnieżdżonych kluczy Definiowanie zagnieżdżonych dokumentów Ustawianie indeksów Metody pośredniczące Sprawdzanie stanu modelu Zapytania Rozszerzanie zapytań Sortowanie Wybieranie danych Limitowanie wyników Pomijanie wyników Automatyczne wypełnianie kluczy Konwersja typów
Przykład Mongoose Konfiguracja aplikacji Refaktoryzacja Definiowanie modeli
Podsumowanie
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
221 223 224 224 224 228 230 232 233 234 235 235
236 236 238 238 239 239 239 240 240 240 240 241 241 241 242
242 242 243 243
245
9
SPIS TREŚCI Rozdział 13 MySQL node-mysql Konfiguracja Aplikacja Express Łączenie z MySQL Inicjalizacja skryptu Wstawianie danych Pobieranie danych
Narzędzie Sequelize Konfiguracja Sequelize Konfiguracja aplikacji Express Konfiguracja Sequelize Definiowanie modeli i synchronizacja Wstawianie danych Pobieranie danych Usuwanie danych Wykończenie
Podsumowanie
10
Rozdział 14 Redis Instalacja Redis Język zapytań Redis Typy danych Ciągi znaków Tablice asocjacyjne Listy Zbiory Zbiory sortowane
Redis i Node Implementacja mapy relacji przy użyciu Node i Redis
Część V
247 248 248 248 249 250 253 258
259 260 260 263 264 266 268 269 271
272 273 275 275 276 277 277 279 279 280
280 281
Podsumowanie
290
Testowanie
291
Rozdział 15 Współdzielony kod Co może być współdzielone? Kompatybilność kodu JavaScript Udostępnianie modułów Adaptacja interfejsów programistycznych ECMA Adaptacja interfejsów programistycznych Node Adaptacja interfejsów programistycznych przeglądarek Dziedziczenie dla wszystkich przeglądarek
Zastosowanie praktyczne: narzędzie browserbuild Prosty przykład
Podsumowanie
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
293 294 294 295 296 297 298 298
299 300
302
SPIS TREŚCI Rozdział 16 Testowanie Proste testy Przedmiot testów Strategia testów Program testowy
Expect.js Przegląd interfejsów programistycznych
Mocha Testowanie asynchronicznego kodu Styl BDD Styl TDD Styl eksportu Korzystanie z Mocha w przeglądarce
305 306 306 306 307
308 308
310 311 313 314 314 315
Podsumowanie
316
Skorowidz
317
11
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
Przedmowa
WIĘKSZOŚĆ APLIKACJI SIECIOWYCH działa zarówno po stronie klienta, jak i po stronie serwera. Do tej pory implementacja strony serwera bywała złożona i z reguły kłopotliwa. Utworzenie prostego serwera wymagało zaawansowanej wiedzy na temat wielowątkowości, skalowalności i wdrożenia serwera. Dodatkowym utrudnieniem był fakt, iż oprogramowanie klienta sieciowego implementowane jest przy użyciu HTML i JavaScript, podczas gdy implementacja kodu serwera wymaga z reguły użycia bardziej statycznych języków programowania. Rozłam ten narzuca programiście konieczność korzystania z wielu języków i wymusza na nim podjęcie decyzji odnośnie umiejscowienia konkretnych części aplikacji już na wczesnym etapie jej powstawania. Jeszcze kilka lat temu implementacja oprogramowania serwera za pomocą JavaScript byłaby nie do pomyślenia. Niska wydajność czasu wykonania, prymitywne zarządzanie pamięcią i brak integracji z systemem operacyjnym powodowały, że JavaScript nie mógł być brany pod uwagę jako realne rozwiązanie dla serwerów. Aby rozwiązać pierwsze dwa problemy, jako część Google Chrome, zaprojektowaliśmy nowy silnik JavaScript V8. V8 jest dostępny w formie projektu open source i oferuje prosty interfejs programistyczny, dzięki któremu może zostać osadzony. Ryan Dahl dostrzegł okazję wykorzystania JavaScript po stronie serwera, osadzając silnik V8 w warstwie integracji z systemem operacyjnym, która używała asynchronicznych interfejsów poszczególnych systemów. Tak narodził się Node.JS. Korzyści były oczywiste. Programista mógł się teraz posługiwać jednym językiem programowania zarówno po stronie klienta, jak i po stronie serwera. Dynamiczny charakter JavaScript bardzo uprościł proces tworzenia kodu serwera i eksperymentowania z nim, uwalniając programistę od tradycyjnego, powolnego i opartego na wielu narzędziach modelu programowania. Node.JS odniósł błyskawiczny sukces, przyczyniając się do powstania prężnej społeczności, wspierając firmy, a nawet doczekując się swojej własnej konferencji. Wszystko to osiągnął dzięki kombinacji prostoty, większej produktywności programowania oraz wysokiej wydajności. Pewną satysfakcję daje mi fakt, że do sukcesu Node.JS przyczynił się również silnik V8. Ta książka przeprowadzi czytelnika poprzez wszystkie etapy tworzenia aplikacji sieciowej po stronie serwera na bazie Node.JS, łącznie z organizacją asynchronicznego kodu serwera oraz pracy z interfejsami baz danych. Życzę miłej lektury, Lars Bak, Wirtualny Maszynista
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
Wstęp
Pod koniec 2009 roku na konferencji JavaScript w Berlinie Ryan Dahl zaprezentował technologię nazwaną Node.JS (http://nodejs.org/). Co ciekawe, ku zaskoczeniu uczestników, technologia ta nie została stworzona z myślą o przeglądarkach internetowych, dotychczasowym obszarze wpływów JavaScript, do którego, zdaniem wielu programistów, JavaScript miał już pozostać na zawsze ograniczony. Nowa technologia przewidywała wykonywanie kodu JavaScript na serwerze. Pomysł ten pobudził wyobraźnię publiczności, która przyjęła go owacją na stojąco. Jeśli wszystko pójdzie dobrze, będziemy mogli pisać aplikacje sieciowe w tylko jednym języku. Taka była niewątpliwie pierwsza myśl każdego uczestnika. W końcu do stworzenia bogatej i nowoczesnej aplikacji sieciowej biegła znajomość JavaScript jest konieczna, ale technologie serwera różnią się między sobą i wymagają specjalizacji. Facebook ujawnił ostatnio na przykład, że biorąc pod uwagę liczbę wierszy kodu w danym języku, JavaScript jest przez niego wykorzystywany cztery razy częściej niż używany po stronie serwera PHP. Przesłanie Ryana było proste, ale miało bardzo dużą siłę oddziaływania. Ryan nie miał jednak zamiaru na nim poprzestać i zaprezentował program „hello world” w technologii Node.JS, który tworzył serwer WWW: var http = require('http'); var server = http.createServer(function (req, res) { res.writeHead(200); res.end('Hello world'); }); server.listen(80);
Powyższy przykład wydaje się być trywialny, ale taki nie jest. Wykorzystany w nim serwer WWW okazuje się być przynajmniej tak samo (o ile nie bardziej) wydajny jak sprawdzone środowiska typu Apache czy Nginx w wielu typowych scenariuszach. Node.JS jest narzędziem ukierunkowanym w szczególności na projektowanie aplikacji sieciowych w poprawny sposób. Node.JS zawdzięcza swoją niewiarygodną szybkość i wydajność technice zwanej pętlą zdarzeń (ang. event loop) i silnikowi V8, na którym jest oparty. Ten ostatni to interpreter i wirtualna maszyna JavaScript, stworzone przez Google w trakcie pracy nad przyspieszaniem przeglądarki Chrome.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
WSTĘP Jeśli chodzi o tworzenie stron WWW, Node.JS zmienia zasady gry. Nie ma już konieczności pisania skryptów, które uruchamiane są przez instalowany osobno serwer WWW, jak w tradycyjnym modelu LAMP, gdzie wykorzystuje się najczęściej PHP i Apache. Jak przekonasz się już wkrótce, odzyskanie kontroli nad serwerem WWW powoduje powstanie zupełnie nowej kategorii aplikacji tworzonych za pomocą Node.JS: aplikacji sieciowych czasu rzeczywistego. Niezwykle szybki transport strumienia danych między serwerem i tysiącami współbieżnych klientów to chleb powszedni dla Node. Oznacza to, że nie tylko masz możliwość tworzenia bardziej efektywnych aplikacji, ale stajesz się też częścią społeczności, która przesuwa granice tego, o czym myśleliśmy, że jest możliwe w internetowym świecie. W Node.JS kontrola należy do nas, a wraz z nią trzymujemy także nowe wyzwania i zadania, które będziemy w tej książce starannie analizować.
ZANIM ZACZNIEMY Podręcznik Node.js jest przede wszystkim książką o JavaScript. Znajomość tego języka jest niezbędna do zrozumienia większości omawianych zagadnień i dlatego pierwszy rozdział poświęcam w całości przybliżeniu problematyki JavaScript, kładąc nacisk na najważniejsze moim zdaniem pojęcia.
16
Jak z czasem się przekonasz, jednym z założeń Node.JS jest stworzenie środowiska, w którym programista będzie czuł się komfortowo. Powszechnie używane wyrażenia spoza specyfikacji języka, które zostały dodane przez twórców przeglądarek, jak na przykład setTimetout czy console.log, są również do dyspozycji programisty Node.JS. Po zakończeniu krótkiego „kursu odświeżającego” przejdziemy od razu do Node. Node zawiera wiele różnych modułów podstawowych i rewolucyjnie prosty menedżer pakietów, nazwany NPM. Ta książka nauczy Cię korzystać zarówno z podstawowych modułów Node, jak i z wybranych najbardziej użytecznych abstrakcji zbudowanych przez społeczność nad modułami podstawowymi, które zainstalujesz za pomocą NPM. Przed przystąpieniem do omówienia modułu zaprojektowanego w celu rozwiązania konkretnego problemu, staram się pokazać, jak pokonać daną przeszkodę bez jego pomocy. Kluczem do zrozumienia narzędzia jest zrozumienie, dlaczego jest ono potrzebne. Dlatego zanim poznasz framework sieciowy, dowiesz się, dlaczego warto go używać zamiast metod HTTP. Zanim nauczysz się tworzyć aplikacje z wykorzystaniem zgodnego z przeglądarkami frameworka czasu rzeczywistego, jak Socket.IO, poznasz ograniczenia WebSockets opartego na szkieletowym HTML5. Książka bazuje na przykładach. Na każdym etapie będziemy tworzyć małe aplikacje i testować poszczególne interfejsy. Kod każdego przykładu z tej książki wykonasz za pomocą polecenia node, którego możesz użyć na dwa sposoby:
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
WSTĘP
Korzystając z narzędzia REPL (ang. Read-Eval-Print Loop — pętla wczytaj-wykonaj-wypisz), które podobnie jak konsole JavaScript narzędzi Firebug i Web Inspector pozwalają na wprowadzenie kodu JavaScript i jego szybkie wykonanie po naciśnięciu klawisza Enter, bezpośrednio z poziomu interfejsu wiersza poleceń systemu.
Jako pliki przetwarzane poleceniem node. Ta metoda wymaga użycia edytora tekstu, który na pewno posiadasz. Osobiście polecam do tego celu Vim (http://www.vim.org), ale wystarczy dowolny inny edytor.
W większości przypadków kod będzie tworzony stopniowo, poprzez modyfikację jego poprzednich wersji. Przeprowadzę Cię przez najtrudniejsze etapy w tym procesie refaktoringu. W kluczowych punktach pojawią się zrzuty ekranu, przedstawiające to, co powinieneś widzieć w oknie terminala albo w oknie przeglądarki (w zależności od tego, czym w danej chwili będziesz się zajmować). Mimo niemałego wysiłku włożonego w konstrukcję tych przykładów, pojawienie się problemów od czasu do czasu jest nieuniknione. Oto zbiór zasobów, które mogą okazać się pomocne:
ZASOBY Jeżeli jakaś część tej książki sprawi Ci szczególne trudności, pomoc uzyskasz na kilka sposobów.
17 Dużo informacji natury ogólnej odnośnie Node.JS znajdziesz na:
grupie dyskusyjnej Node.JS (http://groups.google.com/group/nodejs),
kanale #nodejs serwera irc.freenode.net.
Pomoc związaną z konkretnymi projektami, jak na przykład socket.io lub express, najłatwiej uzyskać oficjalnymi kanałami. Fora o tematyce ogólnej, na przykład Stack Overflow (http://stackoverflow.com/questions/tagged/node.js), z pewnością też okażą się przydatne. Większość modułów Node.JS przechowywana jest w repozytoriach GitHub. Jeśli znajdziesz błąd, co do którego masz absolutną pewność, znajdź odpowiednie repozytorium GitHub i prześlij przypadek testowy. Postaraj się zidentyfikować źródło swojego problemu. Zdarza się, że nie jest nim Node.JS, ale JavaScript. Prośby o pomoc do społeczności Node.JS najlepiej kierować tylko w tym pierwszym przypadku. Jeśli chcesz przedyskutować konkretne zagadnienie z tej książki, znajdziesz mnie pod adresem
[email protected].
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
I CZĘŚĆ
SZYBKI START: INSTALACJA I POJĘCIA OGÓLNE
Rozdział 1. „Przygotowanie środowiska” Rozdział 2. „Przegląd JavaScript” Rozdział 3. „Blokujące i nieblokujące operacje wejścia/wyjścia” Rozdział 4. „JavaScript dla Node”
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
1 ROZDZIAŁ
PODRĘCZNIK NODE.JS
PRZYGOTOWANIE ŚRODOWISKA
INSTALACJA NODE.JS TO bezbolesny proces. Od samego początku jednym z podstawowych założeń tej technologii było utrzymanie małej liczby zależności, co miało przyczynić się do sprawnej i bezproblemowej kompilacji i instalacji projektu.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
Ten rozdział opisuje proces instalacji dla systemów Windows, OS X oraz Linux. W tym ostatnim przypadku należy zapewnić poprawność zależności i skompilować Node.JS ze źródła.
CZĘŚĆ I: SZYBKI START: INSTALACJA I POJĘCIA OGÓLNE
Uwaga: W tej książce znak $ na początku wiersza fragmentu kodu oznacza, że wyrażenie powinno zostać wpisane w wierszu poleceń powłoki systemowej.
INSTALACJA W SYSTEMIE WINDOWS Jeżeli korzystasz z systemu Windows, przejdź do strony http://nodejs.org i pobierz plik instalatora. Każda wersja Node posiada swój własny plik instalatora, który należy pobrać, a następnie uruchomić. Pliki nazywane są zgodnie ze schematem node-v?.?.?.msi. Po uruchomieniu pliku postępuj według instrukcji kreatora instalacji pokazanego na rysunku 1.1. Aby skontrolować poprawność instalacji, otwórz wiersz poleceń, uruchamiając cmd.exe, i wpisz $ node –version.
22
Rysunek 1.1. Kreator instalacji Node.JS
Otrzymasz informację z nazwą wersji zainstalowanego przed chwilą pakietu.
INSTALACJA W SYSTEMIE OS X Używając komputera mac, podobnie jak w przypadku maszyny z systemem Windows, możesz skorzystać z pakietu instalacyjnego. Pobierz ze strony Node.JS plik PKG o nazwie w formacie nodev?.?.?.pkg. Jeśli wolisz samodzielną kompilację, upewnij się, że dysponujesz aplikacją XCode, i postępuj zgodnie z instrukcjami kompilacji dla systemu Linux. Uruchom pobrany pakiet i stosuj się do prostych instrukcji (zob. rysunek 1.2).
Rysunek 1.2. Pakiet instalacyjny Node.JS
Aby sprawdzić skuteczność instalacji, otwórz terminal (aplikacja Terminal.app) i wpisz: $ node–version
Powinieneś zobaczyć numer zainstalowanej wersji Node.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 1: PRZYGOTOWANIE ŚRODOWISKA
INSTALACJA W SYSTEMIE LINUX Kompilacja Node.JS jest prawie tak prosta, jak instalacja dystrybucji binarnej. Do kompilacji w większości systemów *nix potrzebować będziesz jedynie kompilatora C/C++ oraz bibliotek OpenSSL. Większość dystrybucji Linuksa posiada menedżer pakietów, ułatwiający ich instalację. Dla dystrybucji Amazon użyjesz na przykład: > sudo yum install gcc gcc-c++ openssl-devel curl
W przypadku Ubuntu instalacja przebiega nieco inaczej. Użyjesz wtedy polecenia: > sudo apt-get install g++ libssl-dev apache2-utils curl
KOMPILACJA Z poziomu terminala systemu operacyjnego wydaj następujące polecenia: Uwaga: W miejsce znaków ? wstaw w przykładzie numer najnowszej dostępnej wersji Node. $ $ $ $ $ $ $
curl -O http://nodejs.org/dist/node-v?.?.?.tar.gz tar -xzvf node-v?.?.?.tar.gz cd node-v?.?.? ./configure make make test make install
Jeśli polecenie mail test zakończy się błędem, zalecam przerwanie instalacji i przesłanie plików dziennika poleceń ./configure, make i make test na grupę dyskusyjną Node.JS.
KONTROLA DZIAŁANIA Uruchom terminal lub jego ekwiwalent, na przykład XTerm, i wpisz polecenie $ node –version. Powinna zostać wyświetlona informacja o wersji świeżo zainstalowanego Node.
NARZĘDZIE REPL NODE Aby uruchomić narzędzie REPL Node, wydaj polecenie node. Spróbuj wykonać jakieś polecenie JavaScript. Na przykład: > Object.keys(global)
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
23
CZĘŚĆ I: SZYBKI START: INSTALACJA I POJĘCIA OGÓLNE
Uwaga: W tej książce znak > na początku wiersza fragmentu kodu oznacza, że wyrażenie powinno zostać wprowadzone w konsoli narzędzia REPL. REPL jest jednym z moich ulubionych narzędzi do szybkiej weryfikacji, czy różne interfejsy programistyczne Node lub zwykłego JavaScript działają zgodnie z oczekiwaniami. Podczas tworzenia większych modułów często przydaje się możliwość sprawdzenia, czy pewne interfejsy programistyczne działają tak, jak powinny. Otwarcie osobnego okna terminala i szybkie oszacowanie kilku wartości za pomocą REPL bardzo w tym pomaga.
WYKONANIE SKRYPTU Podobnie jak większość języków programowania, Node potrafi zinterpretować zawartość pliku. Wystarczy wydać polecenie node wraz ze ścieżką do pliku. Stwórz za pomocą swojego ulubionego edytora tekstu plik o nazwie my-web-server.js i następującej zawartości:
24
var http = require('http'); var serv = http.createServer(function (req, res) { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end('Podręcznik Node!'); }); serv.listen(3000);
Uruchom skrypt: $ node my-web-server.js
Następnie, tak jak na rysunku 1.3, skieruj swoją przeglądarkę pod adres http://localhost:3000.
Rysunek 1.3. Wyświetlenie prostego dokumentu HTML w Node
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 1: PRZYGOTOWANIE ŚRODOWISKA Powyższy fragment kodu pokazuje, że Node jest w stanie utworzyć w pełni funkcjonalny serwer HTTP, wyświetlający prosty dokument HTML. Przykład ten jest zawsze przywoływany przy okazji dyskusji o Node.JS, ponieważ demonstruje ogromny potencjał Node, który do utworzenia serwera WWW o możliwościach porównywalnych do Apache czy IIS potrzebuje tylko kilku wierszy kodu.
NPM Menedżer Pakietów Node, zwany w skrócie NPM (od ang. Node Package Manager), pozwala na łatwe zarządzanie modułami w projektach, umożliwiając między innymi pobieranie pakietów, sprawdzanie zależności, przeprowadzanie testów i instalację narzędzi wiersza poleceń. Efektywność pracy jest szczególnie istotna w przypadku projektów, które opierają się na dodatkowych modułach o pochodzeniu zewnętrznym. NPM został napisany w Node.JS i wchodzi w skład dystrybucji binarnych (instalatora MSI dla Windows i pakietu PKG dla komputerów mac). W przypadku kompilacji Node z plików źródłowych menedżer NPM możesz zainstalować następująco: $ curl http://npmjs.org/install.sh | sh
Poprawność instalacji sprawdzisz natomiast, wydając polecenie: $ npm --version
Powinno ono spowodować wyświetlenie wersji NPM.
INSTALOWANIE MODUŁÓW Aby zilustrować instalację modułu za pomocą NPM, zainstalujemy bibliotekę colors w katalogu my-project, a następnie utworzymy plik index.js: $ mkdir my-project/ $ cd my-project/ $ npm install colors
Upewnij się, że projekt został zainstalowany, sprawdzając istnienie ścieżki node_modules/colors. Następnie dokonaj edycji pliku index.js za pomocą swojego ulubionego edytora: $ vim index.js
Dodaj następującą treść: require('colors'); console.log('podręcznik node'.rainbow);
Efekt powinien być podobny do tego z rysunku 1.4.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
25
CZĘŚĆ I: SZYBKI START: INSTALACJA I POJĘCIA OGÓLNE
Rysunek 1.4. Efekt instalacji modułu
DEFINIOWANIE WŁASNEGO MODUŁU Aby zdefiniować swój własny moduł, musisz utworzyć plik package.json. Definiowanie własnego modułu ma trzy podstawowe zalety:
Pozwala w prosty sposób udostępnić innym wszystkie składowe Twojej aplikacji, bez konieczności dołączania katalogu node_modules. Ponieważ za całość instalacji odpowiada polecenie npm install, dystrybucja tego katalogu nie miałaby większego sensu. Ma to szczególne znaczenie w systemach zarządzania łańcuchem dostaw, takich jak Git.
Ułatwia monitorowanie działających wersji modułów, na których się opierasz. Przykładowo, po utworzeniu danego projektu uruchomienie polecenia npm install colors spowodowałoby instalację biblioteki colors w wersji 0.5.0. Rok później, na skutek zmian API, najnowsza wersja biblioteki mogłaby już nie być kompatybilna z Twoim projektem, a polecenie npm install bez określenia wersji wygenerowałoby błąd.
Umożliwia redystrybucję. Twój projekt spełnił założenia i chcesz się nim podzielić z innymi? Ponieważ masz plik package.json, poleceniem npm publish opublikujesz projekt w rejestrze NPM, dzięki czemu każdy będzie go mógł zainstalować.
26
Usuń z utworzonego wcześniej katalogu my-project katalog node_modules i utwórz plik package.json: $ rm -r node_modules $ vim package.json
Umieść w nim następujący kod: { "name": "my-colors-project" , "version": "0.0.1" , "dependencies": { "colors": "0.5.0" } }
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 1: PRZYGOTOWANIE ŚRODOWISKA
Uwaga: Zawartość pliku musi być poprawnym kodem JSON. Sama zgodność kodu ze składnią JavaScript nie wystarczy. Oznacza to między innymi, że wszelkie wartości tekstowe (łącznie z nazwami właściwości) powinny zostać ujęte w podwójne cudzysłowy. Plik package.json opisuje projekt na potrzeby zarówno Node.JS, jak i menedżera NPM. Jedyne wymagane pola to nazwa projektu (name) i jego wersja (version). Większość projektów posiada obiekt zależności (dependencies), który odwołuje się do innych projektów za pomocą nazw i wersji określonych w plikach package.json tych projektów. Zapisz plik, zainstaluj lokalny projekt i uruchom ponownie plik index.js. $ npm install $ node index
# zauważ, że możesz pominąć ".js"!
W tym przypadku naszą intencją było utworzenie modułu na użytek własny. Jeśli jednak chcesz, możesz go bez trudu opublikować, wydając polecenie: $ npm publish
Aby wskazać Node, którego pliku powinien szukać, gdy ktoś wyda polecenie require ('my-colors-project'), możemy określić właściwość main w pliku package.json:
27
{ "name": "my-colors-project" , "version": "0.0.1" , "main": "./index" , "dependencies": { "colors": "0.5.0" } }
Kiedy już nauczysz się wymuszać na modułach eksport interfejsów programistycznych, właściwość main stanie się bardziej istotna, ponieważ konieczne będzie określenie punktu wejścia dla Twoich modułów (które czasami mogą składać się z więcej niż jednego pliku). Aby poznać wszystkie właściwości, które można umieścić w pliku package.json, wydaj polecenie: $ npm help json
Uwaga: Jeżeli nie zamierzasz nigdy publikować danego projektu, dodaj w pliku package.json właściwość "private": "true". Zapobiegniesz w ten sposób przypadkowej publikacji.
INSTALACJA NARZĘDZI BINARNYCH Dystrybucje niektórych projektów zawierają narzędzia wiersza poleceń, które zostały napisane w Node. W takich przypadkach należy je instalować z parametrem –g.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
CZĘŚĆ I: SZYBKI START: INSTALACJA I POJĘCIA OGÓLNE Framework express, który poznasz w dalszej części tej książki, zawiera na przykład wykonywalne narzędzie do tworzenia projektów. $ npm install -g express
Możesz wypróbować framework, uruchamiając polecenie express: $ mkdir my-site $ cd mysite $ express
Wskazówka: Jeżeli chcesz dystrybuować skrypt w ten sposób, publikując, dodaj właściwość "bin": "./sciezka/do/skryptu" wskazującą wykonywalny plik skryptu lub plik binarny.
PRZESZUKIWANIE REJESTRU NPM Kiedy już oswoisz się z systemem modułów Node.JS (omówimy go szerzej w rozdziale 4.), będziesz w stanie pisać programy wykorzystujące praktycznie dowolne moduły.
28
Menedżer NPM posiada obszerny rejestr, zawierający tysiące modułów. Przy przeszukiwaniu rejestru kluczowymi poleceniami są search i view. Chcąc na przykład odszukać wtyczki związane ze słowem realtime1, wydasz następujące polecenie: $ npm search realtime
Przeszukane zostaną wszystkie opublikowane moduły, które zawierają to słowo w swojej nazwie, tagach i polu opisu. Kiedy już zlokalizujesz interesujący Cię pakiet, możesz zobaczyć jego plik package.json razem z innymi właściwościami odnoszącymi się do rejestru NPM, wpisując polecenie npm view wraz z nazwą modułu. Na przykład: $ npm view socket.io
Wskazówka: Jeśli chcesz dowiedzieć się więcej na temat polecenia npm, wpisz npm help . Przykładowo, npm help publish dostarczy Ci informacji na temat służącego do publikacji modułów polecenia publish.
1
z ang. „czasu rzeczywistego” — przyp. tłum.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 1: PRZYGOTOWANIE ŚRODOWISKA
PODSUMOWANIE Po przeczytaniu tego rozdziału powinieneś już dysponować działającym środowiskiem Node.JS + NPM. Oprócz umiejętności korzystania z poleceń node i npm, powinieneś potrafić uruchamiać proste skrypty, jak również tworzyć moduły wraz z zależnościami. Wiesz teraz, że w Node.JS ważnym słowem kluczowym jest require. Umożliwia ono współpracę modułów i interfejsów programistycznych i zostanie szerzej przedyskutowane w rozdziale 4., po krótkim omówieniu podstaw języka. Znasz już także rejestr NPM, będący swego rodzaju wrotami do systemu modułów Node.JS. Node.JS jest projektem typu open source, podobnie jak wiele powstających dzięki niemu aplikacji, które są gotowe do użycia i dostępne na wyciągnięcie ręki (a raczej na kliknięcie myszki).
29
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
2 ROZDZIAŁ
PODRĘCZNIK NODE.JS
PRZEGLĄD JAVASCRIPT
WSTĘP JAVASCRIPT JEST obiektowym językiem skryptowym o słabym typowaniu (ang. loosely-typed), bazującym na prototypach. Posiada doskonałe rozwiązania w obszarze funkcji, takie jak domknięcia (ang. closures) i funkcje wyższego rzędu (ang. higher-order functions), którymi zajmiemy się również tutaj. Z technicznego punktu widzenia JavaScript jest implementacją standardu języka ECMAScript. W kontekście Node jest to ważna informacja, jako że, z uwagi na silnik v8, mamy tu do czynienia z implementacją, która, wyłączając kilka dodatkowych mechanizmów, jest bardzo zbliżona do standardu. Oznacza to, że JavaScript używany przy pracy z Node różni się w pewnych miejscach od tego, który przyczynił się do złej sławy języka w świecie przeglądarek. Ponadto, tworzony przez Ciebie kod będzie wykorzystywał „mocne strony” JavaScript, opublikowane przez Douglasa Crockforda w jego słynnej książce JavaScript — mocne strony1. 1
Helion, Gliwice 2011 — przyp. red.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
Ten rozdział został podzielony na dwie części: Podstawowy JavaScript. Elementarne składowe języka. Ma on zastosowanie zarówno przy Node, jak i przy przeglądarkach internetowych. Jest zgodny ze standardami. JavaScript w wersji v8. Pewne elementy języka nie są obsługiwane przez wszystkie przeglądarki, a zwłaszcza przez Internet Explorera, ponieważ dopiero niedawno zostały włączone do standardu. Inne są niestandardowe, ale powszechnie używane, jako że służą do rozwiązywania istotnych problemów. W następnym rozdziale omówimy rozszerzenia języka i jego elementy specyficzne dla Node.
CZĘŚĆ I: SZYBKI START: INSTALACJA I POJĘCIA OGÓLNE
PODSTAWOWY JAVASCRIPT Ten rozdział zakłada pewną znajomość JavaScript i jego składni. Omawia pojęcia i konstrukcje, których zrozumienie jest niezbędne do pracy z Node.
TYPY Typy w JavaScript można podzielić na dwie grupy: proste (ang. primitive) i złożone (ang. complex). Pracując z typem prostym, uzyskujemy bezpośredni dostęp do jego wartości. W przypadku typu złożonego operujemy wyłącznie na referencji do wartości.
Typy proste to number, boolean, string, null i undefined.
Typy złożone to array, function i object.
Zilustrujmy to przykładem: // typy proste var a = 5; var b = a; b = 6; a; // będzie równe 5 b; // będzie równe 6
32
// typy złożone var a = ['witaj', 'świecie']; var b = a; b[0] = 'pa'; a[0]; // będzie równe 'pa' b[0]; // będzie równe 'pa'
W drugim przykładzie b zawiera tę samą referencję do zmiennej co a. Modyfikując pierwszy element tablicy, zmieniamy oryginał, tak więc a[0] === b[0].
TYPOWA ŁAMIGŁÓWKA Poprawna identyfikacja typu danej wartości potrafi być w JavaScript wyzwaniem. Ponieważ JavaScript, tak jak inne języki obiektowe, posiada konstruktory dla większości typów prostych, łańcuch znaków można utworzyć na dwa sposoby: var a = 'fiu'; var b = new String('fiu'); a + b; // 'fiu fiu'
Jeżeli jednak zastosujesz operatory typeof oraz instanceof do tych dwóch zmiennych, zaobserwujesz coś ciekawego: typeof a; // 'string' typeof b; // 'object'
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 2: PRZEGLĄD JAVASCRIPT a instanceof String; // false b instanceof String; // true
Obie zmienne są jednak na pewno łańcuchami o tych samych metodach prototypowych: a.substr == b.substr; // true
Porównanie zmiennych za pomocą operatora == zdaje się to potwierdzać, ale dokonane przy użyciu === już nie: a == b; // true a === b; // false
Mając na uwadze te rozbieżności, proponuję zawsze definiować typy w sposób dosłowny, rezygnując z operatora new. Przy pracy z kodem JavaScript bardzo ważne jest, żeby pamiętać, że niektóre wartości zostaną potraktowane w instrukcjach warunkowych jako false. Są to null, undefined, '' i 0: var a = 0; if (a) { // to się nigdy nie wykona } a == false; // true a === false; // false
Warto również zauważyć, że typeof nie rozpoznaje null jako osobnego typu: typeof null == 'object'; // true, niestety
To samo dotyczy tablic, nawet zdefiniowanych przy użyciu [], tak jak poniżej: typeof [] == 'object'; // true
Wypada się cieszyć, że v8 pozwala na identyfikację tablicy bez potrzeby uciekania się do sztuczek. W przeglądarkach dokonuje się tego najczęściej, badając wewnętrzną wartość [[Class]] obiektu: Object.prototype.toString.call([]) == '[object Array]'. Jest to niezmienna wartość, którą cechuje to, że działa w różnych kontekstach (na przykład w ramkach, w przeglądarce internetowej), podczas gdy instanceof Array zwraca true tylko w danym kontekście.
FUNKCJE Funkcje odgrywają bardzo istotną rolę w języku JavaScript. Są obywatelami pierwszej kategorii2: mogą być przechowywane w zmiennych jako referencje i przekazywane jak każdy inny obiekt: var a = function () {} console.log(a); // przekazujemy funkcję jako parametr 2
Określenie wymyślone przez Christophera Stracheya w latach 60. ubiegłego wieku — przyp. tłum.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
33
CZĘŚĆ I: SZYBKI START: INSTALACJA I POJĘCIA OGÓLNE W JavaScript każda funkcja może zostać nazwana. Nie należy mylić nazwy funkcji z nazwą zmiennej: var a = function a () { 'function' == typeof a; // true };
KONSTRUKCJE THIS, CALL() I APPLY() Po wywołaniu poniższej funkcji wartością this jest globalny obiekt. W przeglądarce internetowej jest nim obiekt okna window: function a () { window == this; // true; }; a();
Używając metod .call i .apply, możesz sprawić, że podczas wywoływania funkcji this będzie referencją do innego obiektu:
34
function a () { this.a == 'b'; // true } a.call({ a: 'b' });
Jedyna różnica między call a apply polega na tym, że call przyjmuje listę parametrów przekazywanych do poprzedzającej ją funkcji, podczas gdy apply oczekuje tablicy: function a (b, c) { b == 'pierwszy'; // true c == 'drugi'; // true } a.call({ a: 'b' }, 'pierwszy', 'drugi') a.apply({ a: 'b' }, ['pierwszy', 'drugi']);
ARNOŚĆ FUNKCJI Interesującą własnością funkcji jest jej arność, czyli liczba argumentów, z którą funkcja została zadeklarowana. W JavaScript jest ona tożsama z właściwością length funkcji: var a = function (a, b, c); a.length == 3; // true
Chociaż rzadziej wykorzystywana w przeglądarkach, ta własność funkcji jest dla nas ważna, ponieważ pewne popularne frameworki Node.JS używają jej, oferując różną funkcjonalność w zależności od liczby argumentów przyjmowanych przez przekazywaną funkcję.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 2: PRZEGLĄD JAVASCRIPT
DOMKNIĘCIA W JavaScript każde wywołanie funkcji powoduje zdefiniowanie nowego zakresu. Zmienne są dostępne tylko w tym zakresie, w którym zostały zdefiniowane, i w zakresach wewnętrznych (czyli zakresach zdefiniowanych w tym zakresie): var a = 5; function woot () { a == 5; // true var a = 6; function test () { a == 6; // true } test(); }; woot();
Funkcje samowywołujące (ang. self-invoked functions) stanowią mechanizm, dzięki któremu można zadeklarować i wywołać anonimową funkcję jedynie po to, żeby uzyskać nowy zakres: var a = 3; (function () { var a = 5; })(); a == 3; // true
Funkcje te są bardzo przydatne, kiedy zachodzi potrzeba deklaracji prywatnych zmiennych, które nie powinny być dostępne z poziomu innych części kodu.
KLASY W JavaScript słowo kluczowe class nie istnieje. Zamiast tego klasę definiuje się jako funkcję: function Animal () { } // zwierzę
Aby zdefiniować metodę dla każdej utworzonej instancji klasy Animal, umieszcza się ją w prototypie: Animal.prototype.eat = function (food) { // metoda eat }
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
35
CZĘŚĆ I: SZYBKI START: INSTALACJA I POJĘCIA OGÓLNE Należy podkreślić, że wewnątrz funkcji zdefiniowanych w prototypie this nie odnosi się do globalnego obiektu (tak jak ma to miejsce w przypadku zwykłych funkcji), ale do instancji klasy: function Animal (name) { this.name = name; } Animal.prototype.getName () { return this.name; }; var animal = new Animal('tobiasz'); a.getName() == 'tobiasz'; // true
DZIEDZICZENIE JavaScript umożliwia dziedziczenie prototypowe. Klasyczne dziedziczenie najczęściej symuluje się w opisany poniżej sposób. Definiujemy kolejny konstruktor, który będzie dziedziczył po klasie Animal: function Ferret () { };
36
// fretka
Aby zdefiniować łańcuch dziedziczenia, inicjalizujemy obiekt Animal i przypisujemy go do właściwości Ferret.prototype. // dziedziczenie Ferret.prototype = new Animal();
Możemy definiować metody i właściwości należące wyłącznie do klasy pochodnej: // definicja właściwości type dla wszystkich obiektów typu Ferret Ferret.prototype.type = 'domowe';
Przesłaniając metodę rodzica, odnosimy się do prototypu: Ferret.prototype.eat = function (food) { Animal.prototype.eat.call(this, food); // ciało metody klasy Ferret }
Ta technika jest niemal idealna. Sprawdza się najlepiej (w porównaniu z alternatywnymi technikami symulowania dziedziczenia) i pozwala zachować własności operatora instanceof: var animal = new Animal(); animal instanceof Animal // true animal instanceof Ferret // false var ferret = new Ferret(); ferret instanceof Animal // true ferret instanceof Ferret // true
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 2: PRZEGLĄD JAVASCRIPT Główną wadą tego rozwiązania jest inicjalizacja obiektu w momencie deklaracji dziedziczenia (Ferret.prototype = new Animal), co nie zawsze jest pożądane. W takim przypadku można umieścić w konstruktorze instrukcję warunkową: function Animal (a) { if (false !== a) return; // zasadnicza część konstruktora } Ferret.prototype = new Animal(false)
Innym sposobem obejścia tego problemu jest zdefiniowanie nowego, pustego konstruktora i przesłonięcie jego prototypu: function Animal () { // zasadnicza część konstruktora } function f () {}; f.prototype = Animal.prototype; Ferret.prototype = new f;
Na szczęście, v8 oferuje gotowe rozwiązanie w tym zakresie, opisane w dalszej części tego rozdziału.
37
BLOK TRY {} CATCH {} Konstrukcja try/catch pozwala przechwycić wyjątek. Poniższy kod spowoduje zgłoszenie wyjątku: > var a = 5; > a() TypeError: Property 'a' of object # is not a function
Kiedy funkcja zgłasza błąd, wykonanie zostaje przerwane: function () { throw new Error('cześć'); console.log('cześć'); // to się nie wykona }
Umieszczając kod w bloku try/catch, można tego uniknąć i obsłużyć błąd: function () { var a = 5; try { a(); } catch (e) { e instanceof Error; // true } console.log('udało się tu dojść!'); }
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
CZĘŚĆ I: SZYBKI START: INSTALACJA I POJĘCIA OGÓLNE
JAVASCRIPT W WERSJI V8 Do tej pory omówiliśmy elementy języka JavaScript wykorzystywane podczas pracy w większości środowisk (w tym również podczas pracy ze starszymi przeglądarkami). Wraz z nadejściem przeglądarki internetowej Chrome pojawił się nowy silnik JavaScript: v8. Zapewniając ekstremalnie szybkie i zawsze aktualne środowisko oraz wykorzystując najnowsze mechanizmy ECMAScript, pozwala on na przekroczenie granic dotychczasowych możliwości. Niektóre z tych mechanizmów mają za zadanie eliminować niedostatki języka. Inne zostały wprowadzone dzięki frameworkom po stronie klienta, takim jak jQuery czy PrototypeJS, ponieważ część dostarczanych przez nie rozszerzeń i narzędzi jest tak często używana, że trudno już sobie wyobrazić język JavaScript bez nich. W tym podrozdziale poznasz najbardziej przydatne mechanizmy i konstrukcje v8, które pomogą Ci tworzyć bardziej zwięzły i szybszy kod, na wzór tego używanego przez najpopularniejsze frameworki i biblioteki Node.JS.
METODA KEYS() OBIEKTU Aby uzyskać wartości kluczy (a i c) następującego obiektu
38
var a = { a: 'b', c: 'd' };
w normalnej sytuacji wykorzystasz iterację: for (var i in a) { }
Przetwarzając iteracyjnie klucze, możesz je zgrupować w tablicy. Jeśli jednak rozszerzysz Object.prototype w następujący sposób: Object.prototype.c = 'd';
to aby uniknąć c na liście kluczy, musisz sprawdzić istnienie właściwości za pomocą hasOwnProperty: for (var i in a) { if (a.hasOwnProperty(i)) {} }
V8 eliminuje tę komplikację. Do bezpiecznego uzyskania wszystkich kluczy obiektu wystarczy użyć: var a = { a: 'b', c: 'd' }; Object.keys(a); // ['a', 'c']
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 2: PRZEGLĄD JAVASCRIPT
METODA ISARRAY() TABLICY Jak już wspomniano wcześniej, operator typeof zwraca wartość "object" dla tablic. Najczęściej jednak chcemy sprawdzić, czy tablica jest rzeczywiście tablicą. Array.isArray zwraca true dla tablic i false dla wszystkich innych wartości: Array.isArray(new Array) // true Array.isArray([]) // true Array.isArray(null) // false Array.isArray(arguments) // false
METODY TABLIC Aby przetworzyć tablicę w pętli, możesz użyć konstrukcji forEach (odpowiednika $.each z jQuery): // wyświetli 1 2 i 3 [1, 2, 3].forEach(function (v) { console.log(v); });
Aby zastosować do tablicy filtr, użyj konstrukcji filter (odpowiednika $.grep z jQuery): [1, 2, 3].filter(function (v) { return v < 3; }); // zwróci [1, 2]
Aby zmienić wartość wszystkich elementów, skorzystaj z konstrukcji map (odpowiednika $.map z jQuery): [5, 10, 15].map(function (v) { return v * 2; }); // zwróci [10, 20, 30]
Metody reduce, reduceRight i lastIndexOf są również dostępne, chociaż rzadziej stosowane.
METODY ŁAŃCUCHÓW ZNAKÓW Aby usunąć spacje z początku i końca łańcucha, użyj: '
serwus
'.trim(); // 'serwus'
JSON V8 udostępnia metody JSON.stringify i JSON.parse, służące do odpowiednio deserializacji i serializacji obiektu JSON.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
39
CZĘŚĆ I: SZYBKI START: INSTALACJA I POJĘCIA OGÓLNE JSON jest formatem przekazywania danych o specyfikacji przypominającej literał obiektu JavaScript, często wykorzystywanym przez liczne usługi sieciowe i interfejsy programistyczne: var obj = JSON.parse('{"a":"b"}') obj.a == 'b'; // true
METODA BIND() FUNKCJI Konstrukcja .bind (odpowiednik $.proxy z jQuery) pozwala na zmianę referencji this: function a () { this.hello == 'world'; // true }; var b = a.bind({ hello: 'world' }); b();
WŁAŚCIWOŚĆ NAME FUNKCJI V8 obsługuje niestandardową właściwość name, zawierającą nazwę funkcji: var a = function woot () {}; a.name == 'woot'; // true
40
V8 używa tej właściwości przy śladach stosu wywołań. Po zgłoszeniu błędu wyświetla ślad stosu wywołań (ang. stack trace), będący serią wywołań funkcji do momentu wystąpienia błędu: > var woot = function () { throw new Error(); }; > woot() Error at [object Context]:1:32
W tej sytuacji v8 nie był w stanie przypisać nazwy do referencji funkcji. Jeśli ją jednak nazwiemy, odpowiednia informacja zostanie umieszczona również na stosie wywołań, co pokazano poniżej: > var woot = function buggy () { throw new Error(); }; > woot() Error at buggy ([object Context]:1:34)
Ponieważ używanie nazw bardzo usprawnia proces usuwania błędów, zalecałbym nazywanie wszystkich swoich funkcji.
WŁAŚCIWOŚĆ __PROTO__ I DZIEDZICZENIE Właściwość __proto__ ułatwia definicję łańcucha dziedziczenia: function Animal () { } function Ferret () { } Ferret.prototype.__proto__ = Animal.prototype;
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 2: PRZEGLĄD JAVASCRIPT Ta bardzo przydatna konstrukcja eliminuje potrzebę:
Korzystania z pośrednich konstruktorów, jak czyniliśmy to we wcześniejszym przykładzie w tym rozdziale.
Sięgania po narzędzia wspomagające programowanie obiektowe. Dołączanie zewnętrznych modułów nie jest wcale konieczne do deklaracji dziedziczenia prototypowego.
METODY DOSTĘPOWE Możesz zdefiniować właściwości, które wywołają funkcję przy próbie dostępu (za pomocą __defineGetter__) i próbie ustawienia wartości (za pomocą __defineSetter__). Jako przykład zdefiniujesz właściwość ago, która będzie informować o czasie, jaki upłynął od pewnego zdarzenia, zwracając wartość tekstową … temu dla obiektu Date. Przy tworzeniu oprogramowania bardzo często się zdarza, że potrzebujesz wyrazić słownie czas względem pewnego punktu. Informacja o tym, że coś zdarzyło się trzy sekundy temu, jest dużo łatwiejsza do odczytania i zrozumienia przez ludzi niż pełna data. Następny przykład dodaje do wszystkich instancji Date metodę zwracającą ago, która zwróci w formie słownej odległość punktu w czasie od chwili obecnej. Sam dostęp do właściwości spowoduje wywołanie zdefiniowanej funkcji, nie trzeba będzie robić tego bezpośrednio. // Na podstawie metody prettyDate Johna Resiga (licencja MIT) Date.prototype.__defineGetter__('ago', function () { var diff = (((new Date()).getTime() - this.getTime()) / 1000) , day_diff = Math.floor(diff / 86400); return day_diff == 0 && ( diff < 60 && "przed chwilą" || diff < 120 && "około minuty temu" || diff < 3600 && Math.floor( diff / 60 ) + " minut(y) temu" || diff < 7200 && "około godziny temu" || diff < 86400 && Math.floor( diff / 3600 ) + " godzin(y) temu") || day_diff == 1 && "Wczoraj" || day_diff < 7 && day_diff + " dni temu" || Math.ceil( day_diff / 7 ) + " tygodni(e) temu"; });
Teraz wystarczy się już tylko odwołać do właściwości ago. Zauważ, że mimo iż nie wywołujesz funkcji, i tak zostaje ona niejawnie wywołana: var a = new Date('12/12/1990'); // moja data urodzenia a.ago // 1071 tygodni(e) temu
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
41
CZĘŚĆ I: SZYBKI START: INSTALACJA I POJĘCIA OGÓLNE
PODSUMOWANIE Zrozumienie tego rozdziału jest niezbędne do rozpoczęcia zmagań z niedoskonałościami języka i większości środowisk, w których był on dotychczas używany, takich jak starsze przeglądarki. Ponieważ JavaScript ewoluował bardzo powoli i przez długie lata był w pewnym sensie zaniedbywany, wielu programistów zainwestowało znaczące ilości czasu w rozwój technik pozwalających tworzyć optymalny i łatwy w utrzymaniu kod, a te aspekty języka, z których działania nie byli zadowoleni, zostały szczegółowo opisane. Silnik v8 zmienił ten stan rzeczy, wprowadzając na bieżąco rozwiązania z najnowszych specyfikacji ECMA. Zespół programistów Node.JS zadbał o to, żeby każda instalacja nowej wersji Node zawierała aktualną wersję v8. Otwiera to przed programowaniem po stronie serwera nowe perspektywy, jako że możemy teraz korzystać z interfejsów programistycznych, które są łatwiejsze do zrozumienia i działają szybciej. Wiedza, którą powinieneś wynieść z lektury tego rozdziału, jest powszechnie wykorzystywana przez programistów Node. Omówione w nim zagadnienia są istotne z punktu widzenia zarówno teraźniejszości, jak i przyszłości JavaScript.
42
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
3 ROZDZIAŁ
PODRĘCZNIK NODE.JS
BLOKUJĄCE I NIEBLOKUJĄCE OPERACJE WEJŚCIA-WYJŚCIA
WIĘKSZA CZĘŚĆ DYSKUSJI o Node.JS koncentruje się wokół jego dużych możliwości w zakresie obsługi współbieżności. Ujmując rzecz najprościej, Node jest frameworkiem, dzięki któremu programiści mogą projektować aplikacje sieciowe o bardzo
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
wysokiej wydajności, w porównaniu z innymi popularnymi rozwiązaniami. Muszą mieć przy tym jednak świadomość kompromisów, jakich wymaga Node, a także wiedzieć, co sprawia, że aplikacje Node są takie wydajne.
CZĘŚĆ I: SZYBKI START: INSTALACJA I POJĘCIA OGÓLNE
DUŻE MOŻLIWOŚCI TO DUŻA ODPOWIEDZIALNOŚĆ Node wprowadza do JavaScript zaawansowany mechanizm, którym raczej nie posługiwałeś się zbyt często po stronie klienta: współbieżność stanu dzielonego (ang. shared-state concurrency). Właściwie, mechanizm ten nie funkcjonuje nawet w tradycyjnych modelach tworzenia aplikacji sieciowych, opartych na Apache i mod_php czy Nginx i FastCGI. Używając mniej technicznego języka, w Node musisz uważać na to, w jaki sposób Twoje wywołania zwrotne (ang. callbacks) modyfikują zmienne ze swojego otoczenia (stan), znajdujące się w danej chwili w pamięci. Musisz zatem zwrócić szczególną uwagę na to, jak obsługujesz błędy, które mogą potencjalnie w nieprzewidziany sposób zmienić ten stan i sprawić, że proces nie będzie się nadawał do użytku. Aby w pełni to zrozumieć, wyobraź sobie następującą funkcję, która jest wywoływana za każdym razem, kiedy użytkownik żąda adresu URL /books. Wyobraź sobie, że „stan” jest kolekcją książek, używaną do wyświetlenia ich listy w formie HTML.
44
var books = [ 'Metamorfoza' , 'Zbrodnia i kara' ]; function serveBooks () { // wyświetlam kod HTML klientowi var html = '' + books.join('') + ''; // Jestem paskudny i zaraz zmienię stan! books = []; return html; }
Analogiczny kod PHP ma postać $books = array( 'Metamorfoza' , 'Zbrodnia i kara' ); function serveBooks () { $html = '' . join($books, '') . ''; $books = array(); return $html; }
Zauważ, że w obydwu funkcjach serveBooks zerujemy tablicę books.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 3: BLOKUJĄCE I NIEBLOKUJĄCE OPERACJE WEJŚCIA-WYJŚCIA A teraz wyobraź sobie, że użytkownik wysyła dwa kolejne żądania /books do serwera Node i dwa kolejne żądania do serwera PHP. Spróbuj przewidzieć, co się wtedy stanie:
Node obsłuży pierwsze żądanie i zwróci książki. Drugie żądanie nie zwróci książek.
PHP zwróci książki w obu przypadkach.
Różnica leży u podstaw systemów. Node jest procesem długotrwałym (ang. long-running), podczas gdy Apache tworzy wiele wątków (jeden dla każdego żądania), rozpoczynających się każdorazowo świeżym stanem. W PHP przy kolejnym uruchomieniu interpretera zmienna $books jest ponownie wypełniana wartościami, podczas gdy w Node wywoływana jest ponownie funkcja serveBooks, a zmienna scope pozostaje niezmieniona. +---------------------+ | APACHE | +-+--------+--------+-+ | | | +---+ | +---+ +----+----+ +----+----+ +----+----+ | PHP | | PHP | | PHP | | | | | | | | WĄTEK | | WĄTEK | | WĄTEK | +----+----+ +----+----+ +----+----+ | | | +---------+ +---------+ +---------+ | ŻĄDANIE | | ŻĄDANIE | | ŻĄDANIE | +---------+ +---------+ +---------+
Duże możliwości oznaczają w tym przypadku również większą odpowiedzialność. +-----------------------------------+ | | | | | NODE.JS | | | | PROCES | | | | | | | +----+------------+------------+----+ | | | +---------+ +---------+ +---------+ | ŻĄDANIE | | ŻĄDANIE | | ŻĄDANIE | +---------+ +---------+ +---------+
Warto o tym zawsze pamiętać, jeśli chcemy tworzyć solidne aplikacje Node.JS, które nie napotykają problemów w trakcie działania. Równie ważną kwestią jest zrozumienie, co kryje się pod pojęciami blokujących i nieblokujących operacji wejścia-wyjścia.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
45
CZĘŚĆ I: SZYBKI START: INSTALACJA I POJĘCIA OGÓLNE
BLOKOWANIE Spróbuj odgadnąć, na czym polega różnica w działaniu między następującym skryptem PHP: // PHP print('Witaj'); sleep(5); print('Świecie');
a analogicznym kodem Node: // node console.log('Witaj'); setTimeout(function () { console.log('Świecie'); }, 5000);
Powyższe przykłady różnią się nie tylko składnią (Node.JS używa wywołania zwrotnego), ale też wzorcowo ilustrują rozróżnienie pomiędzy blokującym i nieblokującym kodem. W pierwszym przykładzie metoda PHP sleep() blokuje wątek wykonawczy. W czasie uśpienia program pozostaje bezczynny.
46
Node.JS wykorzystuje natomiast pętlę zdarzeń, dzięki czemu metoda setTimeout jest metodą nieblokującą. Tak więc instrukcja console.log, umieszczona w kodzie tuż po wywołaniu setTimeout, zostanie wykonana natychmiast: console.log('Witaj'); setTimeout(function () { console.log('Świecie'); }, 5000); console.log('Żegnaj'); // ten skrypt wyświetli: // Witaj // Żegnaj // Świecie
Na czym polega wykorzystanie pętli zdarzeń? Node rejestruje zdarzenia i uruchamia nieskończoną pętlę, w której odpytuje jądro, aby uzyskać informację, czy zdarzenia są gotowe do przetworzenia. Jeśli tak jest, uruchamia odpowiadające zdarzeniu wywołanie zwrotne, po czym pętla przechodzi dalej. W przypadku braku oczekujących zdarzeń Node kontynuuje cykl do momentu pojawienia się nowych zdarzeń.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 3: BLOKUJĄCE I NIEBLOKUJĄCE OPERACJE WEJŚCIA-WYJŚCIA Dla odmiany, w świecie PHP wywołanie metody sleep powoduje blokadę wątku wykonawczego na określony czas, w którym żadna inna instrukcja nie zostanie wykonana. Przetwarzanie odbywa się w sposób synchroniczny. Metoda setTimeout, zamiast blokować, rejestruje zdarzenie na przyszłość i pozwala na kontynuację programu, czyniąc go asynchronicznym. Pętla zdarzeń najlepiej ilustruje podejście Node do współbieżności. Ta sama technika, którą zaprezentowaliśmy na przykładzie funkcji setTimeout, znajduje zastosowanie przy operacjach wejścia-wyjścia wykonywanych przez wbudowane moduły, takie jak http czy net. W przykładzie setTimeout Node posługuje się pętlą zdarzeń i po upłynięciu czasu generuje odpowiednie powiadomienie. Analogicznie postępuje przy operacjach wejścia-wyjścia, używając pętli zdarzeń do wygenerowania powiadomienia o deskryptorach plików. Deskryptory plików (ang. file descriptors) są abstrakcyjnymi uchwytami odwołującymi się od otwartych plików, gniazd (ang. sockets), potoków (ang. pipes) i tak dalej. Ogólnie, kiedy Node otrzymuje z przeglądarki żądanie HTTP, połączenie TCP przydziela odpowiedni deskryptor pliku. Jeśli klient wysyła dane na serwer, Node zostaje o tym powiadomiony i uruchamia wywołanie zwrotne w kodzie JavaScript.
JEDNOWĄTKOWY ŚWIAT Należy zauważyć, że Node używa pojedynczego wątku wykonawczego. Zmiana tego stanu rzeczy nie jest możliwa bez pomocy zewnętrznych modułów. Aby zilustrować to zachowanie Node i pokazać jego związek z pętlą zdarzeń, rozważmy następujący przykład: var start = Date.now(); setTimeout(function () { console.log(Date.now() - start); for (var i = 0; i < 2000000000; i++){} }, 1000); setTimeout(function () { console.log(Date.now() - start); }, 2000);
Te dwa wywołania setTimeout mają za zadanie wyświetlenie liczby milisekund, która upłynęła od momentu rozpoczęcia odliczania do momentu wywołania funkcji zwrotnej. Na moim komputerze rezultat wygląda tak, jak na rysunku 3.1. Co się dzieje wewnątrz programu? Pętla zdarzeń jest blokowana przez kod JavaScript. Po przekazaniu pierwszego zdarzenia uruchamiane jest wywołanie zwrotne. Ponieważ w jego trakcie wykonywane są bardzo intensywne obliczenia (bardzo długa pętla for), zanim program przejdzie do kolejnej iteracji pętli zdarzeń, upływają więcej niż dwie sekundy; dlatego limity czasowe podane w kodzie różnią się od rzeczywistej liczby milisekund.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
47
CZĘŚĆ I: SZYBKI START: INSTALACJA I POJĘCIA OGÓLNE
Rysunek 3.1. Program pokazuje, ile milisekund upłynęło do czasu wywołania funkcji zwrotnej. Rezultaty nie pokrywają się z wartościami w kodzie Jest to oczywiście zachowanie niepożądane. Jak już wcześniej wspomniałem, pętla zdarzeń leży u podstaw wszystkich operacji wejścia-wyjścia w Node. Jeżeli metoda setTimeout ulega opóźnieniu, to samo może się stać z przychodzącym żądaniem HTTP i każdą inną operacją wejścia-wyjścia. A to oznacza, że serwer obsługiwałby mniej żądań w ciągu sekundy, co jest równoznaczne ze spadkiem wydajności. Dlatego większość dostępnych modułów Node to moduły nieblokujące, które wykonują swoje zadania asynchronicznie.
48
Skoro mamy tylko jeden wątek wykonawczy (co oznacza, że podczas działania jednej funkcji nie może zostać wykonana żadna inna), skąd bierze się tak duża wydajność Node.JS przy obsłudze współbieżności w sieci? Przykładowo, na zwykłym laptopie prosty serwer HTTP napisany w Node jest w stanie obsłużyć tysiące klientów w ciągu sekundy. Aby zrozumieć, jak to jest możliwe, musimy najpierw wiedzieć, jak działa stos wywołań. Kiedy v8 wywołuje funkcję po raz pierwszy, inicjuje stos wywołań (ang. call stack), zwany też stosem wykonania (ang. execution stack). Jeżeli w ramach tej funkcji wywoływana jest kolejna, v8 odkłada ją na stos wywołań. Rozważmy następujący przykład: function a () { b(); } function b(){};
Stos wywołań w tym przykładzie składa się z elementu „a” i umieszczonego na nim elementu „b”. Po osiągnięciu „b” v8 nie ma już nic do wykonania. Wróćmy do przykładu serwera HTTP: http.createServer(function () { a(); }); function a(){ b(); }; function b(){};
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 3: BLOKUJĄCE I NIEBLOKUJĄCE OPERACJE WEJŚCIA-WYJŚCIA W tym przykładzie za każdym razem, kiedy klient HTTP łączy się z Node, pętla zdarzeń generuje powiadomienie. Wykonywany jest kod funkcji zwrotnej, a stos wywołań przybiera postać „a” > „b”. Ponieważ Node działa w pojedynczym wątku, do momentu zakończenia przetwarzania wszystkich elementów stosu nie można obsłużyć żadnego innego żądania klienta lub HTTP. W takim razie — możesz pomyśleć — maksymalna współbieżność obsługiwana przez Node to 1! I tak rzeczywiście jest. Node nie obsługuje przetwarzania równoległego, ponieważ wymagałoby to wprowadzenia wielu równoległych wątków wykonawczych. Przy założeniu naprawdę szybkiego przetwarzania stosu wywołań nie ma konieczności równoczesnej obsługi więcej niż jednego wątku. To właśnie dlatego tandem złożony z v8 i nieblokujących operacji wejścia-wyjścia sprawdza się tak dobrze: v8 jest naprawdę szybki, jeśli chodzi o wykonywanie kodu JavaScript, a nieblokujące operacje wejścia-wyjścia zapobiegają zawieszeniu się wątku wykonawczego w sytuacji zewnętrznej niepewności (na przykład odczytu z bazy danych lub z dysku twardego). Przykładem użyteczności nieblokujących operacji wejścia-wyjścia w świecie rzeczywistym jest chmura. W większości usług tego typu, takich jak chmura Amazon („AWS”), systemy operacyjne są wirtualizowane, a zasoby dzielone między najemców (ponieważ „wynajmują” one sprzęt komputerowy). Czyli jeśli na przykład dysk twardy pracuje, szukając pliku dla innego najemcy, a Ty również potrzebujesz znaleźć plik, zwiększa się opóźnienie. Ponieważ wydajność dysków twardych przy operacjach wejścia-wyjścia jest trudna do przewidzenia, blokowanie wątku wykonawczego podczas odczytu pliku powodowałoby wolne i mało stabilne działanie naszego programu. Typowym przykładem operacji wejścia-wyjścia w naszych aplikacjach jest pobieranie danych z bazy. Wyobraź sobie sytuację, w której w odpowiedzi na żądanie musisz pobrać pewne dane z bazy. http.createServer(function (req, res) { database.getInformation(function (data) { res.writeHead(200); res.end(data); }); });
W tym przypadku po nadejściu żądania stos wywołania składa się tylko z zapytania do bazy danych. Jako że jest to zapytanie nieblokujące, inicjacja nowego stosu wywołań po zakończeniu operacji wejścia-wyjścia w bazie zależy znowu od pętli zdarzeń. Ale jeśli powiesz Node „poinformuj mnie, jak tylko otrzymasz odpowiedź z bazy danych”, Node może zająć się innymi rzeczami. A konkretnie obsługą kolejnych klientów i żądań HTTP. Sposób, w jaki Node radzi sobie z błędami, wynika bezpośrednio z architektury frameworka. W dalszej części tego rozdziału omówimy obsługę błędów.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
49
CZĘŚĆ I: SZYBKI START: INSTALACJA I POJĘCIA OGÓLNE
OBSŁUGA BŁĘDÓW Przede wszystkim, jak zaobserwowaliśmy już wcześniej, aplikacje Node opierają się na dużych procesach ze stanami dzielonymi. Jeżeli na przykład w trakcie danego wywołania zwrotnego lub żądania HTTP pojawi się błąd, zagrożony jest cały proces: var http = require('http'); http.createServer(function () { throw new Error('To nie zostanie przechwycone') }).listen(3000)
Ponieważ wyjątek nie zostaje przechwycony, przy próbie dostępu do serwera proces kończy się błędem (rysunek 3.2).
50
Rysunek 3.2. Możesz zobaczyć cały stos wywołań, od pętli zdarzeń (IOWatcher) aż po samo wywołanie zwrotne Node zachowuje się w ten sposób, ponieważ stan procesu jest, po nieprzechwyconym wyjątku, niepewny. Jeżeli błąd nie zostanie obsłużony, dalsze wykonywanie programu może mieć nieprzewidziane skutki. Możesz zmienić to zachowanie, dodając metodę obsługi zdarzenia uncaughtException. Nie następuje wtedy przerwanie procesu i zachowujesz kontrolę nad obsługą błędów: process.on('uncaughtException', function (err) { console.error(err); process.exit(1); // opuszczamy proces manualnie });
Zachowanie to jest zgodne z interfejsami programistycznymi generującymi zdarzenia typu error. Spójrz na przykład na poniższy fragment kodu, który tworzy serwer TCP i łączy się z nim za pomocą narzędzia telnet.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 3: BLOKUJĄCE I NIEBLOKUJĄCE OPERACJE WEJŚCIA-WYJŚCIA var net = require('net'); net.createServer(function (connection) { connection.on('error', function (err) { // err jest obiektem typu Error }); }).listen(400);
Wiele wbudowanych modułów Node, jak na przykład http albo net, generuje zdarzenia typu error. Jeśli nie zostaną one obsłużone, zgłaszany jest nieobsłużony wyjątek. Oprócz zdarzeń uncaughtException i error większość interfejsów programistycznych w Node przyjmuje jako parametr wywołanie zwrotne, jeśli pierwszy parametr jest obiektem błędu lub ma wartość null: var fs = require('fs'); fs.readFile('/etc/passwd', function (err, data) { if (err) return console.error(err); console.log(data); });
Pełna obsługa błędów jest niezbędna, jeżeli chcemy tworzyć bezpieczne programy i nie tracić kontekstu, w którym błędy się pojawiają.
ŚLADY STOSÓW WYWOŁAŃ W języku JavaScript po wystąpieniu błędu możesz zobaczyć sekwencję wywołań funkcji, która do tego błędu doprowadziła. Nazywamy ją śladem stosu wywołań. Rozważmy następujący przykład: function c () { b(); }; function b () { a(); }; function a () { throw new Error('here'); }; c();
Uruchom go, aby uzyskać ślad stosu wywołań, taki jak na rysunku 3.3. Na rysunku widać sekwencję wywołań, które doprowadziły do błędu. Zobaczmy teraz, jak będzie to wyglądało w przypadku pętli zdarzeń:
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
51
CZĘŚĆ I: SZYBKI START: INSTALACJA I POJĘCIA OGÓLNE
Rysunek 3.3. Elementy stosu wywołań wyświetlane przez v8 odpowiadają sekwencji wywołań function c () { b(); }; function b () { a(); };
52
function a () { setTimeout(function () { throw new Error('błąd'); }, 10); }; c();
Po uruchomieniu tego kodu (zob. rysunek 3.4) w śladzie stosu wywołań brakuje wartościowych informacji.
Rysunek 3.4. Stos wywołań zaczyna się w punkcie wejścia pętli zdarzeń Z tego samego powodu nie jest możliwe przechwycenie błędu funkcji odłożonej w czasie. Taka próba powoduje zgłoszenie nieobsłużonego wyjątku, a instrukcje w bloku catch nie zostają wykonane.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 3: BLOKUJĄCE I NIEBLOKUJĄCE OPERACJE WEJŚCIA-WYJŚCIA try { setTimeout(function () { throw new Error('błąd'); }, 10); } catch (e) { }
To głównie dlatego pełna obsługa błędów jest w Node.JS taka ważna. Brak dbałości w tym zakresie może powodować pojawienie się błędów trudnych do zlokalizowania, ze względu na niewystarczającą informację o kontekście ich wystąpienia. Trzeba w tym miejscu wspomnieć, że w przyszłych wersjach Node pojawią się mechanizmy pozwalające na łatwiejsze śledzenie błędów zgłoszonych przez asynchroniczne metody obsługi.
PODSUMOWANIE Wiesz już teraz, w jaki sposób trzej aktorzy — pętla zdarzeń, nieblokujące operacje wejścia-wyjścia oraz interpreter v8 — wspólnym wysiłkiem zapewniają programiście interfejsy umożliwiające tworzenie bardzo szybkich aplikacji sieciowych. Node znacznie ułatwia programiście zadanie dzięki pojedynczemu wątkowi wykonawczemu. Specyficzna architektura powoduje, że tworząc aplikacje w Node, nie opłaca się blokować operacji wejścia-wyjścia. Cały stan utrzymywany jest w pojedynczym obszarze pamięci wydzielonym dla wątku, co wymusza dodatkową staranność przy pisaniu programów. Wiesz już także, że nieblokujące operacje wejścia-wyjścia oraz wywołania zwrotne składają się na zupełnie nowy model wykrywania i obsługi błędów, diametralnie różny od tradycyjnego.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
53
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
4 ROZDZIAŁ
PODRĘCZNIK NODE.JS
JAVASCRIPT DLA NODE
PISANIE KODU JAVASCRIPT DLA Node.JS i dla przeglądarki internetowej to dwa krańcowo odmienne doświadczenia. Node, podobnie jak przeglądarki, wykorzystuje podstawy języka do budowy różnych interfejsów programistycznych, które sprawiają, że proces tworzenia kodu na potrzeby aplikacji sieciowych przebiega w możliwie naturalny sposób.
W tym rozdziale zajmiemy się pewnymi interfejsami, które nie są częścią specyfikacji języka, ale które są obecne zarówno w Node, jak i w przeglądarkach. Ale — co ważniejsze — omówimy również wszystkie kluczowe rozszerzenia wprowadzone przez Node.JS, określone w tytule tego rozdziału jako „JavaScript dla Node”. Pierwsza istotna różnica, której się przyjrzymy, dotyczy globalnego obiektu.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
CZĘŚĆ I: SZYBKI START: INSTALACJA I POJĘCIA OGÓLNE
OBIEKT GLOBALNY W przeglądarce rolę globalnego obiektu pełni obiekt okna window. Wszystko, co zadeklarujemy w obiekcie window, staje się automatycznie dostępne dla każdej części kodu. Na przykład setTimeout to w rzeczywistości window.setTimeout, a document to window.document. Node posiada dwa podobne obiekty — obiekt globalny i obiekt procesu:
global: Podobnie jak w przypadku window, każda właściwość dołączona do obiektu global staje się zmienną o zasięgu globalnym.
process: Wszystko, co odnosi się do globalnego kontekstu wykonania, należy do obiektu process. W przeglądarce mamy tylko jeden obiekt okna, natomiast w Node w danym momencie istnieje tylko jeden obiekt procesu. W przeglądarce za nazwę okna odpowiada właściwość window.name. Jej odpowiednikiem w Node jest process.title.
Ze względu na swoje duże możliwości (zwłaszcza jeśli chodzi o programy wiersza poleceń) obiekt process zostanie omówiony szerzej w kolejnych rozdziałach.
POŻYTECZNE ZMIENNE GLOBALNE 56
Niektóre funkcje i narzędzia dostępne w przeglądarkach nie są częścią specyfikacji języka. Zostały one dodane jako użyteczne mechanizmy, a dziś uznaje się je powszechnie za JavaScript. Wiele z nich dostępnych jest w postaci zmiennych globalnych. Metoda setTimeout nie należy na przykład do specyfikacji ECMAScript, tym niemniej została uznana za potrzebną i zaimplementowana przez przeglądarki. Tak naprawdę, odtworzenie tej metody przy użyciu czystego JavaScript nie jest nawet możliwe. Inne interfejsy programistyczne są stopniowo wprowadzane do języka (są na etapie propozycji), Node.JS korzysta z nich jednak już teraz, ponieważ pozwalają na efektywne tworzenie programów. Przykładem takiego interfejsu jest metoda setImmediate, której odpowiednikiem w Node.JS jest process.nextTick. Metoda ta pozwala zaplanować wykonanie funkcji w następnej iteracji pętli zdarzeń: console.log(1); process.nextTick(function () { console.log(3); }); console.log(2);
Możesz o niej myśleć jako o odpowiedniku setTimeout(fn, 1) lub poleceniu „wywołaj tę funkcję w najbliższej przyszłości w sposób asynchroniczny”. Pomoże Ci to zrozumieć, dlaczego powyższy przykład wyświetli cyfry w kolejności 1, 2, 3.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 4: JAVASCRIPT DLA NODE Innym przykładem jest obiekt console, początkowo zaimplementowany przez Firebuga, rozszerzenie Firefoksa dla programistów. W efekcie także Node posiada obiekt console, wraz z przydatnymi metodami, takimi jak console.log czy console.error.
SYSTEM MODUŁÓW JavaScript w swojej czystej formie bazuje w dużej części na zmiennych globalnych. Wszystkie używane na co dzień w przeglądarkach interfejsy programistyczne (setTimeout, document i tak dalej) są zdefiniowane globalnie. Dołączając zewnętrzne moduły, oczekujemy, że również one udostępnią jedną lub więcej zmiennych globalnych. Jeśli na przykład umieścimy w kodzie naszego dokumentu HTML odwołanie do biblioteki jQuery , późniejszy dostęp do tego modułu uzyskamy poprzez globalny obiekt jQuery: jQuery(function () { alert('witaj świecie!'); });
Dzieje się tak przede wszystkim dlatego, że specyfikacja JavaScript nie przewiduje interfejsu programistycznego, który zapewniłby zależność i izolację modułów. W rezultacie dołączanie wielu modułów w taki sposób powoduje zanieczyszczenie globalnej przestrzeni nazw i potencjalne konflikty nazw. W skład Node wchodzi wiele przydatnych modułów, które stanowią obowiązkowy zestaw narzędziowy przy budowie nowoczesnych aplikacji; między innymi http, net czy fs. A za pomocą znanego Ci z rozdziału 1. menedżera pakietów można zainstalować setki innych. Zamiast definiowania zmiennych globalnych (lub korzystania z dużej ilości niepotrzebnego kodu) twórcy Node zdecydowali się wprowadzić prosty, ale niezwykle prężny system modułów, u którego podstaw leżą trzy zmienne globalne: require, module i exports.
MODUŁY WZGLĘDNE I BEZWZGLĘDNE Pod pojęciem modułów bezwzględnych rozumiem te lokalizowane przez Node automatycznie, w wyniku przeszukania katalogu node_modules, oraz wbudowane moduły Node, takie jak na przykład fs. Być może pamiętasz z rozdziału 1., że po instalacji modułu colors jest on dostępny pod ścieżką ./node_modules/colors. W ten sposób możesz go dołączyć, odnosząc się wyłącznie do jego nazwy, bez konieczności wskazywania katalogu: require('colors')
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
57
CZĘŚĆ I: SZYBKI START: INSTALACJA I POJĘCIA OGÓLNE Akurat ten moduł nie udostępnia interfejsu programistycznego, zmieniając tylko właściwość String.prototype. Ale moduł fs udostępnia już kilka przydatnych funkcji: var fs = require('fs'); fs.readFile('/some/file', function (err, contents) { if (!err) console.log(contents); });
System modułów pozwala także na ich wykorzystanie wewnętrzne, w separacji od interfejsów programistycznych i abstrakcji. Ale nie ma konieczności deklarowania każdej części modułu lub aplikacji jako osobnego modułu, z własnym plikiem package.json. Zamiast tego możesz skorzystać z czegoś, co nazywam modułami względnymi. Moduły względne wskazują w poleceniu require plik JavaScript z bieżącego katalogu. Aby zobaczyć to na przykładzie, stwórz w jednym katalogu dwa pliki o nazwach module_a.js i module_b.js i trzeci plik o nazwie main.js.
module_a.js console.log('tutaj a');
58
module_b.js console.log('tutaj b');
main.js require('module_a'); require('module_b');
Następnie uruchom plik main (zob. rysunek 4.1): $ node main
Jak widać na rysunku 4.1, Node nie potrafi odnaleźć modułów module_a i module_b. Dzieje się tak, ponieważ nie zostały one zainstalowane za pomocą menedżera pakietów, nie znajdują się w katalogu node_modules ani też na pewno nie są modułami wbudowanymi Node. W tym przypadku należy poprzedzić nazwy modułów ciągiem znaków ./:
main.js require('./module_a'); require('./module_b');
Uruchom teraz ponownie plik main (rezultat powinien przypominać ten z rysunku 4.2).
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 4: JAVASCRIPT DLA NODE
Rysunek 4.1. Błąd przy próbie dołączenia module_a; nie można znaleźć pliku
59 Rysunek 4.2. Wymagane moduły zostały uruchomione Udało się! Kod dwóch modułów został wykonany. W dalszej kolejności zobaczymy, jak można sprawić, by moduł udostępniał interfejs programistyczny, który można przypisać do zmiennej podczas wywołania require.
UDOSTĘPNIANIE INTERFEJSU PROGRAMISTYCZNEGO W udostępnieniu interfejsu programistycznego przez moduł, w postaci wartości zwracanej przez wywołanie require, biorą udział dwie zmienne globalne: module i exports. Domyślnie każdy moduł eksportuje pusty obiekt {}. Jeśli chcesz dodać do niego właściwości, po prostu użyj obiektu exports:
module_a.js exports.name = 'jan'; exports.data = 'jakieś dane'; var privateVariable = 5;
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
CZĘŚĆ I: SZYBKI START: INSTALACJA I POJĘCIA OGÓLNE exports.getPrivate = function () { return privateVariable; };
Następnie przetestuj działanie modułu (zob. rysunek 4.3):
index.js var a = require('./module_a'); console.log(a.name); console.log(a.data); console.log(a.getPrivate());
Rysunek 4.3. Wyświetlenie wartości udostępnianych przez interfejs programistyczny modułu module_a
60
W tym przypadku exports jest referencją do domyślnego obiektu module.exports. Jeżeli ustawienie wartości poszczególnych kluczy obiektu nie wystarczy, można go nadpisać w całości. Ma to często miejsce w modułach, które eksportują konstruktory (zob. rysunek 4.4):
person.js module.exports = Person; function Person (name) { this.name = name; }; Person.prototype.talk = function () { console.log('nazywam się', this.name); };
index.js var Person = require('./person'); var john = new Person('jan'); john.talk();
Jak widać, w pliku index zwracana wartość nie jest obiektem, ale funkcją, dzięki przesłonięciu module.exports.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 4: JAVASCRIPT DLA NODE
Rysunek 4.4. Obiektowy JavaScript na przykładzie modułu Node.JS
ZDARZENIA Jednym z najważniejszych interfejsów programistycznych w Node.JS jest EventEmitter. Zarówno w Node, jak i po stronie klienta duża część kodu zależy od zdarzeń, których się nasłuchuje i które są emitowane: window.addEventListener('load', function () { alert('Okno załadowane!'); });
Metody DOM, które odpowiadają w przeglądarkach internetowych za zdarzenia, to głównie addEventListener, removeEventListener i dispatchEvent. Posiada je wiele obiektów, od window aż po XMLHTTPRequest. W poniższym przykładzie wysyłamy żądanie AJAX (kod zakłada, że korzystamy z nowoczesnej przeglądarki) i nasłuchujemy zmiany jego stanu (stateChange), aby wiedzieć, kiedy odpowiedź będzie gotowa: var ajax = new XMLHTTPRequest ajax.addEventListener('stateChange', function () { if (ajax.readyState == 4 && ajax.responseText) { alert('mamy trochę danych: ' + ajax.responseText); } }); ajax.open('GET', '/my-page'); ajax.send(null);
W Node nasłuchiwanie i emisja zdarzeń też jest na porządku dziennym. Dlatego Node udostępnia interfejs programistyczny EventEmitter, definiujący metody on, emit i removeListener. Interfejs dostępny jest w postaci obiektu process.EventEmitter:
eventemitter/index.js var EventEmitter = require('events').EventEmitter , a = new EventEmitter; a.on('event', function () { console.log('wywołano zdarzenie'); }); a.emit('event');
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
61
CZĘŚĆ I: SZYBKI START: INSTALACJA I POJĘCIA OGÓLNE Interfejs EventEmitter wymaga większej ilości kodu niż jego odpowiednik z modelu DOM, dlatego Node używa go wewnętrznie, pozwalając w prosty sposób dodać go do swoich klas: var EventEmitter = process.EventEmitter , MyClass = function (){}; MyClass.prototype._proto__ = EventEmitter.prototype;
Dzięki temu wszystkie instancje MyClass są teraz w stanie obsługiwać zdarzenia: var a = new MyClass; a.on('jakieś zdarzenie', function () { // zrób coś });
Zdarzenia są kluczowym elementem koncepcji nieblokowania wątku wykonawczego w Node. Node z reguły nie wysyła odpowiedzi z danymi „natychmiast” (wymagałoby to zablokowania wątku podczas oczekiwania na zasób). Zamiast tego emituje zdarzenia. Jako przykład rozważmy raz jeszcze serwer HTTP. Po uruchomieniu przez Node wywołania zwrotnego przy nadchodzącym żądaniu, nie wszystkie dane muszą być od razu dostępne. Taka sytuacja ma na przykład miejsce przy żądaniach POST (gdy użytkownik wysyła formularz).
62
Kiedy formularz jest wysyłany, standardowo nasłuchujemy zdarzeń data i end żądania: http.Server(function (req, res) { var buf = ''; req.on('data', function (data) { buf += data; }); req.on('end', function () { console.log('Wszystkie dane gotowe!'); }); });
To typowy przypadek użycia w Node.JS: dane odpowiedzi są ładowane do bufora (obsługa zdarzenia data), a kiedy zostaną już w całości odebrane, można je przetworzyć w wybrany sposób (obsługa zdarzenia end). Aby Node był w stanie jak najszybciej poinformować Cię o dotarciu żądania na serwer, bez względu na to, czy odebrano już całość danych, musi polegać na zdarzeniach. W Node mechanizm zdarzeń informuje Cię również o rzeczach, które jeszcze się nie wydarzyły, ale wydarzą się wkrótce. To, czy zdarzenie wystąpi czy nie, zależy od implementującego je interfejsu programistycznego. Wiemy na przykład, że ServerRequest dziedziczy po EventEmitter oraz że emituje zdarzenia data i end. Niektóre interfejsy programistyczne emitują zdarzenia error, które mogą wystąpić lub nie. Są zdarzenia, które występują tylko raz (jak end), lub mogące zajść więcej razy (jak data).
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 4: JAVASCRIPT DLA NODE Czasami zdarzenia są emitowane tylko przy zaistnieniu pewnych okoliczności. Wystąpienie jednego zdarzenia może na przykład gwarantować, że inne już nie wystąpi. W przypadku żądania HTTP po zajściu zdarzenia end nie spodziewamy się kolejnych zdarzeń data. W każdym razie nie w poprawnie zachowującej się aplikacji. Podobnie, czasami w swojej aplikacji potrzebujesz zarejestrować wywołanie zwrotne dla zdarzenia tylko raz, bez względu na to, czy zdarzenie wystąpi ponownie w przyszłości. Node dostarcza specjalną metodę na takie okazje: a.once('an event', function () { // ta funkcja zostanie wywołana tylko raz, nawet jeśli zdarzenie wystąpi ponownie });
Aby dowiedzieć się, jakie zdarzenia są dostępne i jakie kontrakty („zasady” zdefiniowane w danym interfejsie programistycznym odnośnie ich wywoływania) je obowiązują, najlepiej odnieść się do dokumentacji danego modułu. W ramach tej książki poznasz interfejsy programistyczne wbudowanych modułów Node i niektóre najważniejsze zdarzenia, dokumentację warto mieć jednak zawsze pod ręką.
BUFORY Kolejną po braku modularności niedoskonałością języka eliminowaną przez Node jest obsługa danych binarnych. Globalny obiekt Buffer reprezentuje stały przydział pamięci (liczba bajtów zarezerwowanych dla bufora musi być z góry znana) i zachowuje się jak tablica bajtów, pozwalając na reprezentację danych binarnych w JavaScript. Jedną z jego zalet jest możliwość konwersji pomiędzy danymi zakodowanymi w różny sposób. Możesz na przykład stworzyć bufor z reprezentacji obrazu w formacie base64, a następnie zapisać go do pliku jako binarny obraz PNG, który może zostać użyty:
buffers/index.js var mybuffer = new Buffer('R0lGODlhFgAYAIAAAHbRSv///yH5BAAHAP8ALAAAAAAWABgAAAI2j I8AyH0Kl3MxzlTzzBziDkphaIxgaXJoWq2sF7xtLMO1fYu5K/Ovz/qkNqPLQ2UUKpIUyaQAADs=== ii1j2i3h1i23h', 'base64'); console.log(mybuffer); require('fs').writeFile('logo.gif', mybuffer);
Dla tych, którzy nie wiedzą, format base64 jest sposobem zapisu danych binarnych przy wyłącznym użyciu znaków ASCII. Innymi słowy, pozwala wyrazić coś tak złożonego jak obraz za pomocą znaków alfabetu (co, notabene, pochłania dużo więcej miejsca). Większość interfejsów programistycznych Node.JS, które przeprowadzają operacje wejścia-wyjścia, przyjmuje i eksportuje dane w postaci buforów. W powyższym przykładzie metoda writeFile z modułu File System przyjmuje bufor jako parametr, aby zapisać plik logo.gif.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
63
CZĘŚĆ I: SZYBKI START: INSTALACJA I POJĘCIA OGÓLNE Uruchom kod przykładu i otwórz plik (zob. rysunek 4.5). $ node index $ open logo.gif
Rysunek 4.5. Plik GIF utworzony z reprezentacji base64 bufora, przedstawiający logo Node.JS
64
Jak widać, polecenie console.log wywołane z obiektem Buffer spowodowało wyświetlenie surowych bajtów składających się na obraz.
PODSUMOWANIE W tym rozdziale poznałeś najistotniejsze różnice między kodem JavaScript tworzonym na potrzeby przeglądarek internetowych a tym, który pisany jest dla Node. Dokonaliśmy w nim przeglądu interfejsów programistycznych dodanych przez Node, które, chociaż niezwykle przydatne w codziennej pracy z JavaScript, nie są częścią specyfikacji języka. Wśród nich są mechanizmy umożliwiające wywołanie funkcji z opóźnieniem (ang. timers), zdarzenia, dane binarne i moduły. Wiesz już, co jest odpowiednikiem obiektu window w Node, a także potrafisz korzystać z dostępnych narzędzi programistycznych, takich jak obiekt console.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
CZĘŚĆ II
NAJISTOTNIEJSZE INTERFEJSY PROGRAMISTYCZNE NODE
Rozdział 5. „Wiersz poleceń i moduł FS: Twoja pierwsza aplikacja” Rozdział 6. „Protokół TCP” Rozdział 7. „Protokół HTTP”
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
5 ROZDZIAŁ
PODRĘCZNIK NODE.JS
WIERSZ POLECEŃ I MODUŁ FS: TWOJA PIERWSZA APLIKACJA
W TYM ROZDZIALE ZAJMIEMY SIĘ jednymi z najważniejszych interfejsów programistycznych Node.JS: interfejsami związanymi z obsługą strumienia wejściowego (stdin) i wyjściowego (stdout) procesu oraz interfejsami związanymi z systemem plików (moduł fs). Jak już wiemy z poprzedniego rozdziału, kluczowe w sposobie obsługi współbieżności przez Node jest użycie wywołań zwrotnych
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
i zdarzeń. Dzięki tym interfejsom poznasz kontrolę przepływu w procesie programowania z wykorzystaniem zdarzeń i nieblokujących operacji wejścia-wyjścia. Wiedzę na temat tych interfejsów i ich interakcji sprawdzisz, tworząc swoją pierwszą aplikację: prosty, uruchamiany z wiersza poleceń eksplorator plików, który umożliwi użytkownikowi tworzenie nowych oraz odczyt zawartości istniejących plików.
CZĘŚĆ II: NAJISTOTNIEJSZE INTERFEJSY PROGRAMISTYCZNE NODE
WYMAGANIA Na początek określ, jakie zadania powinien wykonywać program:
Chcesz, żeby program uruchamiany był z wiersza poleceń. Oznacza to, że będzie uruchamiany albo za pomocą polecenia node, albo bezpośrednio, a dalsza interakcja z użytkownikiem będzie się odbywać przez terminal.
Po uruchomieniu program powinien wyświetlić listę bieżących katalogów (zob. rysunek 5.1).
Rysunek 5.1. Lista bieżących katalogów wyświetlana przy starcie programu
68
Po wybraniu pliku program powinien wyświetlić jego zawartość.
Po wybraniu katalogu program powinien wyświetlić jego podkatalogi.
Następnie program powinien się zakończyć.
Biorąc pod uwagę powyższe, projekt można rozbić na kilka mniejszych etapów: 1. Utworzenie naszego modułu. 2. Wybranie synchronicznej lub asynchronicznej wersji modułu fs. 3. Zrozumienie strumieni. 4. Przeprowadzenie operacji wejścia i wyjścia. 5. Refaktoring. 6. Interakcja z modułem fs. 7. Dopracowanie szczegółów.
PISZEMY NASZ PIERWSZY PROGRAM Zbudujesz teraz moduł na bazie wymienionych powyżej kroków. Moduł będzie złożony z kilku plików, które utworzysz za pomocą dowolnego edytora tekstu. Pod koniec tego rozdziału będziesz dysponować w pełni funkcjonalnym programem, napisanym w całości w Node.JS.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 5: WIERSZ POLECEŃ I MODUŁ FS: TWOJA PIERWSZA APLIKACJA
TWORZYMY MODUŁ Jak w każdym przykładzie w tej książce, zaczniemy od utworzenia katalogu zawierającego nasz projekt. Na potrzeby przykładu nazwiemy go file-explorer. W poprzednich rozdziałach wspomnieliśmy o dobrej praktyce definiowania pliku package.json dla każdego projektu. Zachowujesz w ten sposób kontrolę nad zależnościami określonymi w rejestrze NPM i możliwość publikacji modułów w przyszłości. Chociaż w naszym przykładzie będziemy korzystać tylko z wbudowanych modułów Node (a więc niepobieranych z rejestru NPM), musimy przygotować prosty plik package.json:
package.json { "name": "file-explorer" , "version": "0.0.1" , "description": "Eksplorator plików w wierszu poleceń!" }
Uwaga: NPM wprowadza numerację kontroli wersji według tzw. konwencji semver. To dlatego zamiast „0.1” lub „1” w polu version podajemy wartość „0.0.1”.
69 Aby zweryfikować poprawność pliku package.json, wydaj polecenie $ npm install. Jeżeli wszystko działa, nie powinny zostać wyświetlone żadne błędy1. W innym razie pojawi się wyjątek JSON (zob. rysunek 5.2).
Rysunek 5.2. Uruchomienie polecenia npm install z niepoprawnym kodem JSON w pliku package.json W następnej kolejności utworzysz plik JavaScript index.js, który będzie zawierał podstawowy kod programu.
1
Aczkolwiek mogą zostać wyświetlone ostrzeżenia — przyp. tłum.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
CZĘŚĆ II: NAJISTOTNIEJSZE INTERFEJSY PROGRAMISTYCZNE NODE
SYNC CZY ASYNC? Na początek zadeklaruj w swoim pliku zależności. Ponieważ interfejsy stdio są częścią zmiennej globalnej process, jedyną zależnością będzie moduł fs:
index.js /** * Zależności modułu. */ var fs = require('fs');
Pierwszym zadaniem po uruchomieniu programu będzie uzyskanie listy plików w bieżącym katalogu. Musisz przy tym pamiętać, że interfejs programistyczny fs jest wyjątkowy w tym sensie, że pozwala zarówno na blokujące, jak i nieblokujące wywołania. Jeśli na przykład chcesz pobrać listę istniejących katalogów, możesz to zrobić w następujący sposób: > console.log(require('fs').readdirSync(__dirname));
Wywołanie zwróci zawartość natychmiast lub wygeneruje wyjątek w przypadku błędu (zob. rysunek 5.3).
70
Rysunek 5.3. Sprawdzanie wartości readdirSync Innym podejściem jest rozwiązanie asynchroniczne: > function async (err, files) { console.log(files); }; > require('fs').readdir('.', async);
Da ono identyczne wyniki, pokazane na rysunku 5.4.
Rysunek 5.4. Asynchroniczna wersja readdir
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 5: WIERSZ POLECEŃ I MODUŁ FS: TWOJA PIERWSZA APLIKACJA Z rozdziału 3. wiemy, że aby nasze aplikacje były szybkie i radziły sobie z obsługą współbieżności w jednym wątku przy dużym obciążeniu, muszą obsługiwać zdarzenia asynchronicznie. Nasz prosty program wiersza poleceń z pewnością nie będzie funkcjonował w takim środowisku (w danym momencie obsługiwać go będzie tylko jedna osoba), ale aby poznać dobrze jedno z najważniejszych i najtrudniejszych zagadnień związanych z Node.JS, zastosujesz rozwiązanie asynchroniczne. Do uzyskania listy plików wykorzystamy zatem metodę fs.readdir. Przekazywane wywołanie zwrotne dostarcza obiekt błędu (który ma wartość null w przypadku braku błędu) i tablicę files:
index.js // . . . fs.readdir(__dirname, function (err, files) { console.log(files); });
Spróbuj wywołać program! Otrzymany rezultat powinien być podobny do tego z rysunku 5.5.
71
Rysunek 5.5. Twój pierwszy program w akcji Teraz, kiedy już wiesz, że moduł fs zawiera zarówno synchroniczne, jak i asynchroniczne metody dostępu do systemu plików, musisz jeszcze poznać fundamentalne dla Node.JS pojęcie, jakim są strumienie.
ZROZUMIENIE STRUMIENI Jak prawdopodobnie zauważyłeś, metoda console.log wyświetla dane w konsoli. A uściślając, console.log wykonuje konkretne zadanie: zapisuje do strumienia wyjścia stdout podany przez użytkownika łańcuch znaków wraz ze znakiem nowego wiersza \n. Zwróć uwagę na różnicę w wyświetlaniu na rysunku 5.6. A teraz spójrz na kod źródłowy:
example-1.js console.log('Witaj świecie');
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
CZĘŚĆ II: NAJISTOTNIEJSZE INTERFEJSY PROGRAMISTYCZNE NODE
Rysunek 5.6. W pierwszym przykładzie po „Witaj świecie” następuje znak nowego wiersza, w drugim już nie oraz
example-2.js process.stdout.write('Witaj świecie');
Globalna zmienna procesu zawiera trzy obiekty Stream, odpowiadające trzem standardowym strumieniom w systemie Unix: - **stdin**: Standard input - **stdout**: Standard output - **stderr**: Standard error
Rolę tych obiektów zilustrowano na rysunku 5.7.
72
Rysunek 5.7. Obiekty stdin, stdout i stderr w kontekście tradycyjnego terminala tekstowego Pierwszy z nich, stdin, jest strumieniem do odczytu, podczas gdy stdout i stderr są strumieniami do zapisu. Domyślnym stanem strumienia stdin jest stan wstrzymania (paused). Z reguły po uruchomieniu program wykonuje pewne zadania, po czym kończy działanie. Czasami jednak, i tak jest również w naszej aplikacji, program oczekuje na dane i przynajmniej dopóki nie zostaną one wprowadzone przez użytkownika, nie może zakończyć działania. Kiedy wznawiasz ten strumień (za pomocą metody resume), Node obserwuje odpowiedni deskryptor pliku (który w systemie Unix otrzymuje numer 0) i przy ciągłym działaniu pętli zdarzeń nie kończy programu, czekając na wywołanie zdarzeń. Node.JS zawsze kończy działanie automatycznie, chyba że oczekuje na dane wejścia-wyjścia.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 5: WIERSZ POLECEŃ I MODUŁ FS: TWOJA PIERWSZA APLIKACJA Inną ciekawą własnością obiektu Stream jest to, że posiada on domyślne kodowanie. Jeśli ustawisz kodowanie dla strumienia, zamiast surowego obiektu Buffer otrzymasz zakodowany łańcuch tekstowy (za pomocą UTF-8, ASCII itd.) jako parametry zdarzeń. Obiekt Stream jest podstawowym elementem wykorzystywanym przy budowie aplikacji w Node, podobnie jak obiekt EventEmitter (po którym zresztą dziedziczy). Podczas pracy z Node często będziesz się spotykać z różnego rodzaju strumieniami, takimi jak gniazda TCP czy żądania HTTP. W skrócie, wszędzie tam, gdzie mamy do czynienia ze stopniowym odczytem lub zapisem danych, obecne są strumienie.
WEJŚCIE I WYJŚCIE Teraz, kiedy masz już pewne pojęcie o tym, co dzieje się po uruchomieniu programu, możesz przystąpić do tworzenia pierwszej części aplikacji. Wyświetli ona listę plików w bieżącym katalogu i poczeka na dane wprowadzane przez użytkownika:
index.js // . . . fs.readdir(process.cwd(), function (err, files) { console.log(''); if (!files.length) { return console.log(' } console.log('
\033[31m Brak plików do wyświetlenia!\033[39m\n');
Wybierz plik lub katalog, który chcesz zobaczyć\n');
function file(i) { var filename = files[i]; fs.stat(__dirname + '/' + filename, function (err, stat) { if (stat.isDirectory()) { console.log(' '+i+' \033[36m' + filename + '/\033[39m'); } else { console.log(' '+i+' \033[90m' + filename + '\033[39m'); } i++; if (i == files.length) { console.log(''); process.stdout.write(' process.stdin.resume(); } else { file(i); } }); } file(0); });
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
\033[33mWprowadź swój wybór: \033[39m');
73
CZĘŚĆ II: NAJISTOTNIEJSZE INTERFEJSY PROGRAMISTYCZNE NODE Przeanalizujmy ten kod wiersz po wierszu. Aby zwiększyć przejrzystość tekstu, wstawiamy pusty wiersz: console.log('')
Następnie dodajemy komunikat o braku plików do wyświetlenia, jeśli tablica plików jest pusta. Łańcuchy \033[31m i 033[39m, otaczające tekst, nadają mu czerwony kolor. Na końcu znajduje się znak nowego wiersza \n, służący do wizualnego rozdzielenia tekstu. if (!files.length) { return console.log(' }
\033[31m Brak plików do wyświetlenia!\033[39m\n');
Kolejnego wiersza nie trzeba objaśniać: console.log('
Select which file or directory you want to see\n');
Definiujemy funkcję, która będzie wywołana dla każdego elementu tablicy. Jest to pierwszy ze wzorców asynchronicznej kontroli przepływu używanych w tej książce: przetwarzanie wsadowe (ang. serial execution). Pod koniec rozdziału zajmiemy się nim bardziej szczegółowo.
74
function file (i) { // . . . }
Uzyskujemy dostęp do pierwszej nazwy pliku i pobieramy informacje o pliku w postaci obiektu Stat. Obiekt fs.stat dostarcza nam różne metadane pliku lub katalogu: var filename = files[i]; fs.stat(__dirname + '/' + filename, function (err, stat) { // . . . });
Funkcja zwrotna dostarcza nam obiekt błędu (o ile taki się pojawi) oraz obiekt Stat. W tym przypadku interesuje nas metoda isDirectory tego ostatniego: if (stat.isDirectory()) { console.log(' '+i+' } else { console.log(' '+i+' }
\033[36m' + filename + '/\033[39m'); \033[90m' + filename + '\033[39m');
Jeśli ścieżka jest katalogiem, zostanie wyświetlona w innym kolorze niż pliki. Dalej następuje najważniejsza część kontroli przepływu. Zwiększamy indeks o jeden, bezpośrednio po czym sprawdzamy, czy pozostały jeszcze jakieś pliki do przetworzenia:
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 5: WIERSZ POLECEŃ I MODUŁ FS: TWOJA PIERWSZA APLIKACJA i++; if (i == files.length) { console.log(''); process.stdout.write(' \033[33mWprowadź swój wybór: \033[39m'); process.stdin.resume(); process.stdin.setEncoding('utf8'); } else { file(i); }
Jeżeli nie ma już więcej plików, użytkownik proszony jest o wybór opcji. Zauważ, że posługujemy się tu metodą process.stdout.write zamiast console.log; nie chcemy przenosić kursora do nowego wiersza, użytkownik wprowadza swój wybór bezpośrednio po komunikacie (zob. rysunek 5.8): console.log(''); process.stdout.write('
\033[33mWprowadź swój wybór: \033[39m');
Rysunek 5.8. Aktualna wersja programu prosi o wprowadzenie danych wejściowych Jak już wiesz, poniższy wiersz pozwala na pobranie danych od użytkownika: process.stdin.resume();
W tym wierszu ustawiamy kodowanie strumienia na wartość utf-8, zapewniając obsługę znaków specjalnych i diakrytycznych: process.stdin.setEncoding('utf8');
Jeśli są jeszcze pliki do przetworzenia, nasza funkcja zostaje wywołana w sposób rekurencyjny ponownie: file(i);
Proces jest kontynuowany, dopóki wszystkie pliki nie zostaną przetworzone, po czym użytkownik proszony jest o wprowadzenie danych. Tym sposobem najważniejsza część aplikacji jest już prawie gotowa.
REFAKTORING Refaktoring zaczniemy od dodania przydatnych skrótów, jako że stdin i stdout będą przez nas używane stosunkowo często.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
75
CZĘŚĆ II: NAJISTOTNIEJSZE INTERFEJSY PROGRAMISTYCZNE NODE index.js // . . . var fs = require('fs') , stdin = process.stdin , stdout = process.stdout
Ponieważ kod jest asynchroniczny, ryzykujemy, że wraz z rozbudową programu (szczególnie jeśli będzie związana z kontrolą przepływu) zbyt głębokie zagnieżdżenie funkcji zmniejszy czytelność kodu. Aby temu zapobiec, możesz oddzielnie zdefiniować funkcje reprezentujące poszczególne etapy asynchronicznego procesu. Na początek wyodrębnij funkcję odczytującą stdin:
index.js // wywoływana dla każdego pliku w katalogu function file(i) { var filename = files[i]; fs.stat(__dirname + '/' + filename, function (err, stat) { if (stat.isDirectory()) { console.log(' '+i+' \033[36m' + filename + '/\033[39m'); } else { console.log(' '+i+' \033[90m' + filename + '\033[39m'); }
76
if (++i == files.length) { read(); } else { file(i); } }); } // odczytaj dane użytkownika po wyświetleniu plików function read () { console.log(''); stdout.write(' \033[33mWprowadź swój wybór: \033[39m'); stdin.resume(); stdin.setEncoding('utf8'); }
Zwróć uwagę, że wykorzystujesz również nowe zmienne pomocnicze stdin i stdout. Po odczytaniu danych następnym logicznym krokiem jest ich przetworzenie. Użytkownik jest proszony o wybranie pliku, który ma zostać odczytany. Po ustawieniu kodowania dla strumienia stdin, zaczynamy nasłuchiwać zdarzenia data:
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 5: WIERSZ POLECEŃ I MODUŁ FS: TWOJA PIERWSZA APLIKACJA function read () { // . . . stdin.on('data', option); } // wywoływana z opcją wybraną przez użytkownika function option (data) { if (!files[Number(data)]) { stdout.write(' \033[31mWprowadź swój wybór: \033[39m'); } else { stdin.pause(); } }
Sprawdzamy tutaj, czy istnieje indeks tablicy files odpowiadający wyborowi użytkownika. Pamiętaj, że tablica files jest częścią wywołania zwrotnego (fs.readdir), w obrębie którego cały czas się znajdujesz. Zwróć też uwagę na konwersję łańcucha utf-8 data do typu Number przed dokonaniem sprawdzenia. Jeżeli indeks tablicy istnieje, strumień musi zostać ponownie wstrzymany (wracając do stanu domyślnego), aby — po wykonaniu operacji fs, opisanych w kolejnym kroku — program mógł zakończyć działanie (zob. rysunek 5.9).
77
Rysunek 5.9. Przykład źle wprowadzonego wyboru Teraz, kiedy nasz program jest już zdolny do interakcji z użytkownikiem, prezentując mu listę plików do wyboru, możemy zająć się ich odczytem i wyświetleniem.
INTERAKCJA Z MODUŁEM FS Kod odpowiedzialny za odszukiwanie plików jest gotowy, czas zatem na ich odczyt! function option (data) { var filename = files[Number(data)]; if (!filename) { stdout.write(' \033[31mWprowadź swój wybór: \033[39m'); } else { stdin.pause(); fs.readFile(__dirname + '/' + filename, 'utf8', function (err, data) { console.log('');
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
CZĘŚĆ II: NAJISTOTNIEJSZE INTERFEJSY PROGRAMISTYCZNE NODE console.log('\033[90m' + data.replace(/(.*)/g, ' });
$1') + '\033[39m');
} }
Zauważ, że również tym razem możesz określić kodowanie z góry, otrzymując gotowy do użycia łańcuch tekstowy: fs.readFile(__dirname + '/' + filename, 'utf8', function (err, data) {
Zawartość data odczytywana jest za pomocą wyrażenia regularnego (zob. rysunek 5.10): data.replace(/(.*)/g, '
$1')
78
Rysunek 5.10. Przykład odczytu prostego pliku Co jeśli użytkownik wybrał katalog? W takiej sytuacji muszą zostać wyświetlone podkatalogi i pliki, które zawiera. Aby uniknąć wielokrotnego wywoływania fs.stat, wróć do funkcji file i dodaj instrukcję zapisującą odwołania do obiektów Stats: // . . . var stats = []; function file(i) { var filename = files[i]; fs.stat(__dirname + '/' + filename, function (err, stat) { stats[i] = stat; // . . .
Teraz możesz sprawdzić, czy użytkownik wybrał katalog w funkcji option. W miejscu, w którym wcześniej znajdowało się wywołanie fs.readFile, wstaw:
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 5: WIERSZ POLECEŃ I MODUŁ FS: TWOJA PIERWSZA APLIKACJA if (stats[Number(data)].isDirectory()) { fs.readdir(__dirname + '/' + filename, function (err, files) { console.log(''); console.log(' (plików: ' + files.length + ')'); files.forEach(function (file) { console.log(' ' + file); }); console.log(''); }); } else { fs.readFile(__dirname + '/' + filename, 'utf8', function (err, data) { console.log(''); console.log('\033[90m' + data.replace(/(.*)/g, ' $1') + '\033[39m'); }); }
Jeśli uruchomisz teraz program, po wybraniu katalogu zobaczysz listę plików, które mogą zostać odczytane, do wyboru (zob. rysunek 5.11).
79
Rysunek 5.11. Przykład odczytu katalogu /test I to już wszystko! Właśnie napisałeś swój pierwszy program wiersza poleceń w Node.
WIERSZ POLECEŃ Masz już za sobą pierwszy program wiersza poleceń, warto zatem poznać kolejne interfejsy programistyczne, pomocne w tworzeniu podobnych aplikacji, uruchamianych w terminalu.
OBIEKT ARGV Obiekt process.argv zawiera wartości wszystkich argumentów, z jakimi program Node został uruchomiony:
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
CZĘŚĆ II: NAJISTOTNIEJSZE INTERFEJSY PROGRAMISTYCZNE NODE example.js console.log(process.argv);
Na rysunku 5.12 widzimy, że pierwszym elementem jest zawsze node, a drugim ścieżka do uruchamianego pliku. Kolejne elementy są argumentami podanymi w poleceniu.
Rysunek 5.12. Przykładowa zawartość process.argv Aby pominąć pierwsze dwa elementy, użyj metody slice (zob. rysunek 5.13):
example-2.js console.log(process.argv.slice(2));
80
Rysunek 5.13. Przykład okrojonej wersji obiektu argv, zawierającej tylko argumenty podane przy uruchomieniu programu Kolejną bardzo ważną rzeczą przy pracy z Node jest zrozumienie różnicy pomiędzy katalogiem, w którym program rezyduje, a katalogiem, w którym jest uruchamiany.
KATALOG ROBOCZY W przykładowej aplikacji z tego rozdziału za pomocą stałej __dirname odwołujesz się do katalogu, w którym znajduje się w systemie plików uruchamiany plik. Czasami jednak w trakcie pracy aplikacji bardziej korzystne jest pobranie nazwy bieżącego katalogu roboczego (ang. current working directory). Zgodnie z aktualną implementacją, niezależnie od tego, czy znajdujesz się w katalogu macierzystym, czy w dowolnym innym katalogu, uruchomienie aplikacji da taki sam wynik. Położenie pliku index.js się nie zmienia, a więc wartość __dirname też pozostaje taka sama. Aby uzyskać bieżący katalog roboczy, wywołaj metodę process.cwd: > process.cwd() /Users/guillermo
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 5: WIERSZ POLECEŃ I MODUŁ FS: TWOJA PIERWSZA APLIKACJA Node umożliwia również jego zmianę, dzięki metodzie process.chdir: > process.cwd() /Users/guillermo > process.chdir('/') > process.cwd() /
Kolejny aspekt kontekstu, w którym uruchamiany jest program, to obecność zmiennych środowiskowych. W następnym punkcie pokażemy, jak uzyskać dostęp do tych zmiennych.
ZMIENNE ŚRODOWISKOWE Node pozwala na łatwy dostęp do zmiennych, które są częścią środowiska powłoki, poprzez wygodny obiekt process.env. Przykładem popularnej zmiennej środowiskowej jest NODE_ENV (zob. rysunek 5.14), której najczęstszym zastosowaniem jest informowanie programu Node, czy działa w środowisku produkcyjnym, czy deweloperskim.
81
Rysunek 5.14. Zmienna środowiskowa NODE_ENV W trakcie działania programu często potrzebna jest bezpośrednia kontrola nad jego zakończeniem.
ZAKAŃCZANIE PROGRAMU Aby zakończyć aplikację, możesz użyć metody process.exit z opcjonalnym kodem zakończenia. Jeśli na przykład chcemy, aby program zakończył się błędem, najlepiej użyć kodu 1. console.error('Wystąpił błąd'); process.exit(1);
Pozwala to na sprawną współpracę pomiędzy programami wiersza poleceń i innymi narzędziami w systemie operacyjnym. Innym ważnym aspektem tej współpracy są sygnały procesu.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
CZĘŚĆ II: NAJISTOTNIEJSZE INTERFEJSY PROGRAMISTYCZNE NODE
SYGNAŁY Proces komunikuje się z systemem operacyjnym na różne sposoby. Jednym z nich są sygnały (ang. signals). Kiedy chcemy na przykład natychmiastowo zakończyć proces, wystarczy mu wysłać sygnał SIGKILL. Sygnały są w Node emitowane jako zdarzenia obiektu process: process.on('SIGKILL', function () { // signal received });
W następnym punkcie wyjaśnimy, jak uzyskaliśmy w naszej przykładowej aplikacji kolorowy tekst.
SEKWENCJE STERUJĄCE ANSI Chcąc kontrolować kolory i inne parametry strumienia wyjściowego w terminalu tekstowym, korzystamy z sekwencji sterujących ANSI (ang. ANSI escape sequences), zwanych również kodami ANSI. Te znaki specjalne są rozpoznawane przez emulator terminala w standardowy sposób.
82
Kiedy umieszczasz między tymi znakami tekst, nie pojawią się one oczywiście na ekranie. Są to tak zwane znaki niedrukowalne (ang. nonprinting characters). Weźmy na przykład następujące sekwencje: console.log('\033[90m' + data.replace(/(.*)/g, '
\033 rozpoczyna sekwencję sterującą;
[ informuje o zmianie koloru;
90 zmienia kolor tekstu na jasnoszary;
m kończy sekwencję.
$1') + '\033[39m');
Zwróć uwagę, że w drugiej sekwencji używamy wartości 39, która powoduje powrót dalszego tekstu do domyślnego dla terminala koloru. Kompletną tabelę kodów ANSI znajdziesz pod adresem http://en.wikipedia.org/wiki/ANSI_ escape_code.
MODUŁ FS Moduł fs umożliwia odczyt i zapis danych poprzez interfejs programistyczny Stream. W przeciwieństwie do metod readFile i writeFile, przydział pamięci odbywa się w jego przypadku stopniowo.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 5: WIERSZ POLECEŃ I MODUŁ FS: TWOJA PIERWSZA APLIKACJA Wyobraź sobie plik z dużą ilością danych oddzielonych przecinkami i milionami wierszy. Jednorazowy jego odczyt w celu przetworzenia wiązałby się z koniecznością przydzielenia dużego obszaru pamięci. Dużo lepszym rozwiązaniem byłby odczyt pliku partiami wyznaczanymi przez znaki końca wiersza („\n”) i ich przetwarzanie na bieżąco. Strumienie Node nadają się do tego idealnie, o czym przekonasz się już zaraz.
STRUMIENIE Metoda fs.createReadStream pozwala utworzyć strumień do odczytu (ang. readable) dla danego pliku. Potencjał strumieni najlepiej ilustruje różnica pomiędzy dwoma zamieszczonymi niżej przykładami: fs.readFile('my-file.txt', function (err, contents){ // zrób coś z plikiem });
W tym przypadku wywołanie przekazywanej funkcji zwrotnej następuje dopiero, kiedy cała zawartość pliku będzie wczytana, umieszczona w pamięci operacyjnej i gotowa do użycia. W poniższym przykładzie natomiast plik odczytywany jest partiami o zmiennym rozmiarze. Funkcja zwrotna wywoływana jest przy odczycie każdej partii: var stream = fs.createReadStream('my-file.txt'); stream.on('data', function(chunk){ // zrób coś z częścią pliku }); stream.on('end', function(chunk){ // osiągnięto koniec pliku });
Dlaczego ta zdolność strumieni jest taka ważna? Wyobraź sobie, że musisz przesłać do usługi sieciowej bardzo duży plik wideo. Wczytanie całego pliku nie jest konieczne do rozpoczęcia przesyłania, tak więc użycie strumienia przekłada się bezpośrednio na szybkość całej operacji. To samo dotyczy zapisu w pliku dziennika, zwłaszcza jeśli korzystamy ze strumienia do zapisu (ang. writable). Jeżeli używasz aplikacji sieciowej do zapisywania działań użytkowników odwiedzających Twoją stronę w pliku dziennika, zmuszanie systemu operacyjnego do każdorazowego otwarcia i zamknięcia pliku (a co za tym idzie, odszukania go na dysku) nie będzie rozwiązaniem efektywnym z racji dużej liczby zapisywanych zdarzeń. W takim przypadku dużo lepiej użyć obiektu fs.WriteStream, otwierając plik raz, a następnie wywołując metodę .write przy każdym nowym wpisie. Kolejnym ważnym elementem modelu pracy Node, polegającego na nieblokowaniu operacji wejścia-wyjścia, jest obserwacja.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
83
CZĘŚĆ II: NAJISTOTNIEJSZE INTERFEJSY PROGRAMISTYCZNE NODE
OBSERWACJA Node umożliwia obserwowanie plików i katalogów pod kątem zmian. Obserwując dany plik lub katalog, jesteśmy informowani (przez zdarzenie w postaci wywołania zwrotnego) o każdej modyfikacji pliku (lub plików zawartych w katalogu). Mechanizm ten jest często wykorzystywany w środowisku Node. Niektórzy wolą na przykład przygotowywać arkusze stylów CSS w sposób pośredni. Wprowadzają oni kod w języku programowania, który jest następnie kompilowany do postaci CSS. Automatyczna kompilacja po każdej modyfikacji pliku jest bardzo wygodna. Rozważmy następujący przykład. Na początek szukamy wszystkich plików CSS w katalogu roboczym, a następnie obserwujemy je pod kątem zmian. Po wykryciu zmiany plik jest wyświetlany w konsoli:
84
var fs = require('fs'); var stream = fs.createReadStream('my-file.txt'); // pobierz wszystkie pliki z katalogu roboczego var files = fs.readdirSync(process.cwd()); files.forEach(function (file) { // obserwuj plik, jeśli kończy się ".css" if (/\.css/.test(file)) { fs.watchFile(process.cwd() + '/' + file, function () { console.log(' – ' + file + ' zmieniony!'); }); } });
Oprócz metody fs.watchFile możesz również skorzystać z metody fs.watch, która pozwala na obserwację całych katalogów.
PODSUMOWANIE W tym rozdziale poznałeś podstawy tworzenia aplikacji w Node.JS, a dokładniej programu wiersza poleceń, który komunikował się z systemem plików. Chociaż ten konkretny program mógł zostać napisany przy użyciu synchronicznych interfejsów modułu fs, skorzystaliśmy z interfejsów asynchronicznych, aby lepiej zrozumieć pewne niuanse tworzenia kodu z dużą liczbą wywołań zwrotnych. Niezależnie od tego udało nam się uzyskać opisowy i w pełni funkcjonalny kod. Omówiony w tym rozdziale jeden z najważniejszych interfejsów programistycznych, Stream, będzie się często przewijał w dalszej części książki. Prawie wszędzie tam, gdzie mamy do czynienia z operacjami wejścia-wyjścia, użycie strumieni jest nieuniknione.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 5: WIERSZ POLECEŃ I MODUŁ FS: TWOJA PIERWSZA APLIKACJA Otrzymałeś też dużo wskazówek i narzędzi, dzięki którym jesteś w stanie pisać złożone i przydatne programy, wykorzystujące system plików, komunikujące się z innymi aplikacjami i pobierające dane od użytkownika. Jako programista Node.JS, będziesz tę wiedzę (a szczególnie jej część odnoszącą się do procesu) wykorzystywać bardzo często, zarówno podczas tworzenia aplikacji sieciowych, jak i podczas rozwiązywania bardziej złożonych problemów. Postaraj się ją zatem dobrze przyswoić!
85
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
6 ROZDZIAŁ
PODRĘCZNIK NODE.JS
PROTOKÓŁ TCP
TCP (Transmission Control Protocol) jest protokołem połączeniowym, który zapewnia usystematyzowany i niezawodny transfer danych z jednego komputera do drugiego. Innymi słowy, TCP to transportowy protokół używany zawsze wtedy, kiedy musimy mieć pewność, że każdy bajt danych wysłanych z jednego punktu dotrze do drugiego w całości i z zachowaniem odpowiedniej kolejności. Między innymi dlatego większość popularnych protokołów, takich jak na przykład HTTP, bazuje na TCP. Kiedy wysyłasz kod HTML strony, chcesz, aby dotarł do celu w niezmienionej postaci, a jeśli to niemożliwe — aby wygenerowany został błąd. Nawet pojedynczy znak (bajt) w złym miejscu mógłby spowodować, że strona nie zostałaby wyświetlona przez przeglądarkę poprawnie. Node.JS jest frameworkiem zaprojektowanym z myślą o aplikacjach działających w sieci. Dzisiejsze aplikacje sieciowe komunikują się za pomocą transmisji TCP/IP. Zrozumienie, na jakiej zasadzie działa protokół TCP/IP i jak w prosty i wygodny sposób może zostać zaimplementowany w Node.JS, jest zatem konieczne.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
Na początek poznasz najważniejsze cechy protokołu. Jakie są na przykład gwarancje przy przesyłaniu danych z jednego komputera na drugi za pomocą TCP? Czy jeśli wyślesz dwa komunikaty z rzędu, dotrą one do odbiorcy w takiej samej kolejności? Zrozumienie protokołu jest niezbędne do korzystania z bazującego na nim oprogramowania. Na przykład za każdym razem, kiedy łączysz się i komunikujesz z bazą danych (dajmy na to MySQL), korzystasz przy tym z gniazda TCP. Serwer HTTP Node jest zbudowany na bazie serwera TCP Node. Dla nas, programistów, najważniejsze jest to, że klasa http.Server dziedziczy po klasie net.Server (net jest modułem TCP). Oprócz przeglądarek internetowych i serwerów (HTTP), z protokołu TCP korzysta dużo popularnych i używanych na co dzień aplikacji, takich jak programy pocztowe (SMPT/IMPAP/ POP3), czaty (IRC/XMPP), klienty SSH i wiele innych. Im więcej będziesz wiedzieć na temat TCP oraz odpowiednich interfejsów programistycznych Node.JS, z tym większą łatwością przyjdzie Ci tworzenie i zrozumienie programów o różnym zastosowaniu działających w środowisku sieciowym.
CZĘŚĆ II: NAJISTOTNIEJSZE INTERFEJSY PROGRAMISTYCZNE NODE
CZYM CHARAKTERYZUJE SIĘ TCP? Aby używać protokołu TCP, nie musisz wcale znać wewnętrznych mechanizmów jego działania ani decyzji, jakie podjęto na etapie jego projektowania. Znajomość tych zagadnień może być jednak bardzo pomocna przy analizie problemów związanych z protokołami i serwerami wyższego poziomu, takimi jak serwery WWW czy bazy danych. Pierwszym ważnym aspektem TCP jest kluczowa rola połączeń w tym protokole.
KOMUNIKACJA Z NACISKIEM NA POŁĄCZENIA I ZASADA ZACHOWANIA KOLEJNOŚCI Pracując z protokołem TCP, możesz myśleć o komunikacji między klientem a serwerem w kategoriach połączenia (ang. connection) lub strumienia danych (ang. data stream). Taka abstrakcja ponad tworzeniem usługi i aplikacji jest bardzo przydatna, ponieważ będący w modelu TCP/IP o poziom niżej protokół IP (Internet Protocol) jest bezpołączeniowy. Protokół IP bazuje na transmisji datagramów (ang. datagrams). Są to pakiety danych wysyłane i odbierane niezależnie, docierające do celu w przypadkowej kolejności.
88
W jaki sposób protokół TCP przekształca te niezależne datagramy w uporządkowany strumień? Jeżeli używanie protokołu IP wiąże się z potencjalnie nieregularnym dostarczaniem pakietów danych, które na dodatek nie należą do żadnego strumienia danych ani połączenia, jak to możliwe, że otwarcie dwóch połączeń TCP/IP do serwera nie powoduje wymieszania pakietów? Odpowiedź na te dwa pytania wyjaśnia sens istnienia protokołu TCP. Kiedy wysyłasz dane w ramach połączenia TCP, przesyłane datagramy IP zawierają informację o połączeniu, do którego należą, a także o ich miejscu w strumieniu danych. Wyobraź sobie podział komunikatu na cztery części. Jeżeli serwer otrzyma części 1. i 4. i obie one należą do połączenia A, wie, że powinien czekać na części 3. i 4., które dotrą w innych datagramach. Tworząc serwer implementujący protokół TCP, na przykład za pomocą Node, nie musisz się przejmować tymi wewnętrznymi zawiłościami. Myślisz w kategorii połączenia, a kiedy zapisujesz dane w gnieździe, wiesz, że druga strona otrzyma je w tej samej kolejności, a pojawienie się błędu sieci spowoduje błąd lub przerwanie połączenia.
KOD BAJTOWY JAKO PODSTAWOWA REPREZENTACJA Protokół TCP nie rozróżnia znaków i kodowań, co jest bardzo dobrym rozwiązaniem. Jak już pokazaliśmy w rozdziale 4., ten sam tekst zakodowany na różne sposoby będzie miał różną długość (w bajtach) podczas transmisji.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 6: PROTOKÓŁ TCP TCP pozwala więc na przesyłanie danych będących ciągiem znaków ASCII (znak o długości 1 bajta) lub tekstu Unicode (znak o długości do 4 bajtów). Nie wymuszając konkretnego formatu komunikatu, TCP oferuje dużą elastyczność.
NIEZAWODNOŚĆ Jako że protokół TCP opiera się na w dużej mierze zawodnej usłudze, musi on implementować szereg mechanizmów bazujących na potwierdzeniach (ang. acknowledgments) i limitach czasowych (ang. timeouts) w celu zapewnienia niezawodności. Po wysłaniu pakietu danych odbiorca wymaga potwierdzenia (krótkiej odpowiedzi wskazującej, że pakiet został odebrany). Jeżeli potwierdzenie nie dotrze w określonym przedziale czasowym, nadawca ponawia próbę wysłania pakietu. Opisane zachowanie skutecznie radzi sobie w niestabilnych warunkach, takich jak błędy i przeciążenia sieci.
KONTROLA PRZEPŁYWU Co jeśli jeden z dwóch komunikujących się komputerów dysponuje wyraźnie szybszym połączeniem? Protokół TCP dba również o równowagę w przepływach pakietów między dwiema stronami komunikacji dzięki kontroli przepływu.
KONTROLA PRZECIĄŻEŃ Protokół TCP posiada wbudowane mechanizmy, których zadaniem jest pilnowanie, aby w sieci nie dochodziło do drastycznego wzrostu odsetka utraconych pakietów i poziomu ich opóźnienia. Dzięki temu możliwe jest utrzymanie wydajności sieci na wysokim poziomie. Podobnie jak w przypadku kontroli przepływu, która zapobiega zdominowaniu odbiorcy przez nadawcę, protokół TCP stara się również zapobiec załamaniu spowodowanemu przeciążeniem sieci, na przykład regulując tempo przesyłu pakietów. Znamy już teoretyczne podstawy działania protokołu TCP, czas zatem na trochę praktyki. Do testowania serwerów TCP wykorzystamy narzędzie Telnet.
TELNET Telnet jest starym protokołem sieciowym zaprojektowanym jako dwukierunkowy wirtualny terminal. Używano go głównie przed wprowadzeniem SSH jako środka umożliwiającego kontrolowanie zdalnych komputerów (na przykład zdalną administrację serwerem). Telnet został zbudowany na bazie (a jakże!) protokołu TCP.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
89
CZĘŚĆ II: NAJISTOTNIEJSZE INTERFEJSY PROGRAMISTYCZNE NODE I chociaż od 2000 roku narzędzie to niemal zupełnie wyszło z użycia, większość współczesnych systemów operacyjnych nadal oferuje klienta telnet (zob. rysunek 6.1): $ telnet
Większa część komunikacji przy użyciu Telnetu odbywa się na porcie 23. Jeżeli spróbujesz połączyć się z serwerem przy użyciu tego portu (telnet host.com 23 lub po prostu telnet host.com), program będzie próbował nadawać protokół Telnet poprzez TCP.
Rysunek 6.1. Uruchamianie narzędzia Telnet Klient telnet ma jednak dużo bardziej interesujące nas możliwości. Jeśli mianowicie widzi, że serwer korzysta przy wysyłaniu danych z protokołu innego niż Telnet, zamiast zamknąć połączenie i wyświetlić błąd, przechodzi w bezprotokołowy tryb RAW TCP.
90
Co się zatem stanie, jeśli połączymy się za pomocą Telnetu z serwerem WWW? Aby to zbadać, rozważmy następujący przykład. Na początek utworzysz w Node prosty, wyświetlający komunikat „Witaj świecie” serwer WWW, który będzie nasłuchiwał na porcie 3000:
web-server.js require('http').createServer(function (req, res) { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end('Witaj świecie'); }).listen(3000);
Uruchomisz go poleceniem node web-server.js. Upewnij się, że działa, korzystając z najbardziej typowego klienta HTTP — przeglądarki internetowej. Efekt powinien być taki, jak na rysunku 6.2. Zaimplementuj teraz klienta. W tym celu nawiąż połączenie za pomocą Telnetu (zob. też rysunek 6.3): $ telnet localhost 3000
Opierając się na rysunku 6.3, możemy stwierdzić, że polecenie zostało wykonane poprawnie, natomiast w oknie terminala nie pojawiło się nic, co by choć trochę przypominało nasz komunikat „Witaj świecie”. Nie stało się tak, ponieważ aby zapisać kod HTML w połączeniu TCP (zwanym również gniazdem), musimy jeszcze utworzyć żądanie HTTP. Wpisz GET / HTTP1.1 i wciśnij dwukrotnie klawisz Enter.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 6: PROTOKÓŁ TCP
Rysunek 6.2. Przeglądarka nawiązuje połączenie TCP z serwerem localhost przez port 3000, a następnie „mówi” przy użyciu protokołu HTTP
Rysunek 6.3. Telnet umożliwia ręczne nawiązanie połączenia TCP za pomocą terminala Powinna się pojawić odpowiedź podobna do przedstawionej na rysunku 6.4.
Rysunek 6.4. Test serwera WWW w Node.JS programem Telnet Reasumując:
Nawiązaliśmy połączenie TCP.
Utworzyliśmy żądanie HTTP.
Otrzymaliśmy odpowiedź HTTP.
Przetestowaliśmy protokół TCP pod kątem możliwości. Dane zostały odebrane w tej samej kolejności, w jakiej je wprowadziliśmy w Node.JS: najpierw pojawił się nagłówek Content-Type, a dopiero później treść odpowiedzi żądania.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
91
CZĘŚĆ II: NAJISTOTNIEJSZE INTERFEJSY PROGRAMISTYCZNE NODE
CZAT NA BAZIE TCP Jak już wiesz, główną ideą protokołu TCP jest umożliwienie niezawodnej komunikacji między komputerami w różnych sieciach. Wybrany w tym rozdziale jako przykład program „Witaj świecie” jest czatem, ponieważ ten rodzaj aplikacji najlepiej ilustruje przydatność protokołu TCP. W dalszej części tego rozdziału utworzysz prosty serwer TCP, z którym połączyć będzie się mógł każdy bez konieczności implementacji skomplikowanych protokołów i poleceń:
Po nawiązaniu połączenia serwer wita Cię i prosi o podanie pseudonimu. Dostajesz też informację o liczbie połączonych klientów.
Po wpisaniu pseudonimu i wciśnięciu klawisza Enter zostajesz zalogowany.
Będąc połączonym, możesz otrzymywać i wysyłać komunikaty od i do innych zalogowanych klientów, wprowadzając tekst i potwierdzając klawiszem Enter.
Co oznacza wciśnięcie klawisza Enter? Zasadniczo wszystko, co wpisujesz w Telnecie, zostaje natychmiast wysłane do serwera. Użycie klawisza Enter powoduje wstawienie znaku \n. Na serwerze Node jest on traktowany jako separator, dzięki któremu wiadomo, że komunikat dotarł w całości.
92
Innymi słowy, użycie klawisza Enter nie różni się niczym od wpisania litery a.
TWORZYMY MODUŁ Jak zwykle zaczniemy od utworzenia katalogu macierzystego dla naszego projektu oraz pliku package.json:
package.json { "name": "czat-tcp" , "description": "Nasz pierwszy serwer TCP" , "version": "0.0.1" }
Sprawdź, czy wszystko działa, wydając polecenie npm install. Program powinien wyświetlić pusty wiersz1, ponieważ nie zdefiniowaliśmy zależności dla projektu.
KLASA NET.SERVER Następnie utwórz plik index.js, który będzie zawierał kod serwera:
1
Może też się zdarzyć, że wyświetli ostrzeżenie — przyp. tłum.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 6: PROTOKÓŁ TCP /** * Zależności modułów. */ var net = require('net') /** * Utwórz serwer. */ var server = net.createServer(function (conn) { // Obsłuż połączenie. console.log('\033[90m nowe połączenie!\033[39m'); }); /** * Nasłuchuj. */ server.listen(3000, function () { console.log('\033[96m serwer nasłuchuje na *:3000\033[39m'); });
Zwróć uwagę, że do metody createServer przekazujesz funkcję zwrotną. Funkcja ta zostanie wywołana za każdym razem, kiedy nawiązane zostanie nowe połączenie z serwerem. Aby przetestować to wywołanie zwrotne, uruchom kod. Metoda listen przydziela serwerowi port 3000, a następnie wyświetla potwierdzający to komunikat. $ node index.js
Rysunek 6.5. Serwerowi zostaje przydzielony port, po czym wyświetla się komunikat potwierdzający Spróbuj teraz połączyć się przez Telnet: $ telnet 127.0.0.1 3000
Na rysunku 6.6 pokazano obok siebie polecenie oraz komunikat „nowe połączenie!”. Jak widzisz, przykład bardzo przypomina program „Witaj świecie”, który używał protokołu HTTP. Nie powinno to być zaskoczeniem, jako że HTTP bazuje na protokole TCP. W tym przypadku jednak tworzymy swój własny protokół.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
93
CZĘŚĆ II: NAJISTOTNIEJSZE INTERFEJSY PROGRAMISTYCZNE NODE
Rysunek 6.6. W pierwszym oknie status procesu serwera; w drugim klient, który po połączeniu powoduje wyświetlenie przez serwer komunikatu „nowe połączenie!” Metoda createServer przekazuje do wywołania zwrotnego instancję często używanego w Node obiektu Stream. W tym przypadku przekazany zostaje obiekt net.Stream, będący strumieniem najczęściej zarówno do odczytu, jak i do zapisu. Ostatnią ważną metodą w naszym kodzie jest metoda listen, która kojarzy serwer z portem. Ponieważ jest ona asynchroniczna, otrzymuje również wywołanie zwrotne.
94
ODBIERANIE POŁĄCZEŃ W opisie projektu założyliśmy, że natychmiast po nawiązaniu połączenia serwer powinien zwrócić do klienta powitanie, wraz z liczbą aktywnych połączeń. Zadeklaruj najpierw poza funkcją zwrotną zmienną licznika: /** * Przechowuj liczbę aktywnych połączeń */ var count = 0;
Następnie zmodyfikuj kod funkcji zwrotnej, który będzie zwiększał licznik i wyświetlał powitanie: var server = net.createServer(function (conn) { conn.write( '\n > witaj na \033[92mczacie node\033[39m!' + '\n > ' + count + ' innych osób jest teraz połączonych.' + '\n > wprowadź swój pseudonim i naciśnij enter: ' ); count++; });
Jak łatwo zauważyć, do wyświetlenia kolorów nadal używamy sekwencji sterujących.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 6: PROTOKÓŁ TCP Przetestuj teraz kod, uruchamiając serwer: $ node index
I połącz się ponownie (zob. rysunek 6.7): $ telnet 127.0.0.1 3000
Rysunek 6.7. Klient po zalogowaniu otrzymuje pewne dane Jeżeli połączysz się ponownie, jak pokazano na rysunku 6.8, licznik połączonych wzrośnie o jeden!
95
Rysunek 6.8. Licznik połączeń w akcji Kiedy klient emituje zdarzenie close, zmienna licznika powinna zostać z kolei zmniejszona o jeden: conn.on('close', function () { count--; });
Zdarzenie close generowane jest przez Node po każdym zamknięciu gniazda. Node.JS posiada dwa zdarzenia związane z finalizacją połączenia: end i close. Pierwsze zostaje wygenerowane, kiedy klient sam zamknie połączenie TCP. Jeżeli na przykład zamkniesz prawidłowo Telnet, wyśle on sygnalizujący koniec połączenia pakiet FIN. Jeśli wystąpi błąd połączenia (który powoduje zdarzenie error), zdarzenie end nie zostanie wygenerowane, ponieważ pakiet FIN nie został odebrany. Zdarzenie close zostanie jednak wygenerowane w obu przypadkach, zatem to jego użyjemy na potrzeby tego przykładu.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
CZĘŚĆ II: NAJISTOTNIEJSZE INTERFEJSY PROGRAMISTYCZNE NODE Do poprawnego zamknięcia połączenia telnet służy kombinacja klawiszy Ctrl+] w systemie Windows oraz Alt+[ na komputerze mac.
ZDARZENIE DATA Po udanym wyświetleniu danych czas teraz zrobić coś z danymi odbieranymi. Pierwszym elementem do obsłużenia jest pseudonim; możemy więc zacząć nasłuchiwać zdarzenia informującego o nadchodzących danych. Podobnie jak wiele interfejsów programistycznych w Node, net.Stream jest również instancją klasy EventEmitter. Aby to sprawdzić, wyświetl na początek przychodzące dane w konsoli serwera: var server = net.createServer(function (conn) { conn.write( '\n > witaj na \033[92mczacie node\033[39m!' + '\n > ' + count + ' innych osób jest teraz połączonych.' + '\n > wprowadź swój pseudonim i naciśnij enter: ' ); count++;
96
conn.on('data', function (data) { console.log(data); }); conn.on('close', function () { count--; }); });
Uruchom następnie serwer i połącz się z nim za pomocą klienta. Spróbuj wprowadzić jakieś dane (zob. dolna część rysunek 6.9). Podczas ich wpisywania serwer wyświetli je poprzez console.log (zob. górna część rysunek 6.9). Jak widzisz, odbierane dane mają postać obiektów Buffer. Czy pamiętasz, co pisałem o protokole TCP i kodzie bajtowym? Tu widać to najlepiej. Co można z tym zrobić? Jest kilka opcji. Możesz na przykład wywołać metodę .toString('utf8') dla obiektu Buffer, aby uzyskać reprezentację bufora zakodowaną w utf-8. Ale ponieważ ani przez chwilę nie będziesz potrzebować danych zakodowanych w jakikolwiek inny sposób, możesz użyć wygodnej metody net.Stream#setEncoding, a Node ustawi odpowiednie kodowanie automatycznie:
index.js //. . . conn.setEncoding('utf8');
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 6: PROTOKÓŁ TCP
Rysunek 6.9. Dane wysyłane w dolnej części są reprezentowane w górnej części jako obiekty Buffer
97 Udało nam się już przesłać dane między klientem a serwerem i w drugą stronę, możemy teraz zatem zająć się śledzeniem pozostałych klientów czatu.
STAN I MONITOROWANIE POŁĄCZEŃ O zdefiniowanej wcześniej zmiennej licznika możemy powiedzieć, że jest częścią stanu. Node obsługuje współbieżność stanu dzielonego, czego odzwierciedleniem w naszym przykładzie jest fakt, iż dwóch współbieżnych użytkowników modyfikuje stan tych samych zmiennych. Aby wysłać wiadomość do wszystkich pozostałych czatujących, musisz rozszerzyć śledzenie stanu o wykaz połączonych użytkowników. Klienta uważa się za połączonego i tym samym za zdolnego do obierania wiadomości, kiedy wprowadzi swój pseudonim. Pierwszym zadaniem jest zatem monitorowanie wszystkich użytkowników, którzy ustalili swój pseudonim. W tym celu wprowadź nową zmienną stanu users: var count = 0 , users = {}
Następnie wprowadź zmienną nickname (która będzie przechowywać pseudonim użytkownika) w zasięgu każdego połączenia:
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
CZĘŚĆ II: NAJISTOTNIEJSZE INTERFEJSY PROGRAMISTYCZNE NODE
Rysunek 6.10. Komunikaty z czatu są wyświetlane w górnej części jako łańcuchy znaków zakodowane w utf-8
98
conn.setEncoding('utf8'); // pseudonim dla bieżącego połączenia var nickname; conn.on('data', function (data) {
Po odebraniu danych oczyść je ze znaków \r\n (odpowiadających klawiszowi Enter): // usuń znak "enter" data = data.replace('\r\n', '');
Jeżeli użytkownik nie ma jeszcze pseudonimu, musimy sprawdzić jego poprawność. Jeśli pseudonim nie jest używany, wyświetlamy wszystkim połączonym komunikat powitalny (zob. rysunek 6.11): // pierwszym oczekiwanym elementem danych jest pseudonim if (!nickname) { if (users[data]) { conn.write('\033[93m> pseudonim jest już używany. spróbuj ponownie:\033[39m '); return; } else { nickname = data; users[nickname] = conn; for (var i in users) { users[i].write('\033[90m > ' + nickname + ' wchodzi do pokoju\033[39m\n');
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 6: PROTOKÓŁ TCP
Rysunek 6.11. Połączenie każdego nowego klienta z serwerem czatu powoduje wyświetlenie odpowiedniego komunikatu u pozostałych } } }
Jeżeli użytkownik ma pseudonim, przychodzące dane traktujemy jako komunikat, który ma zostać przekazany pozostałym: else { // w każdym innym przypadku jest to komunikat czatu for (var i in users) { if (i != nickname) { users[i].write('\033[96m > ' + nickname + ':\033[39m ' + data + '\n'); } } }
Nadawca komunikatu nie powinien go otrzymać, czemu zapobiegamy, sprawdzając warunek i != nickname. Rysunek 6.12 pokazuje nowe zachowanie klientów podczas pisania w jednym i obserwacji drugiego. W naszej aplikacji można już swobodnie wymieniać komunikaty i brakuje już tylko ostatnich szlifów.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
99
CZĘŚĆ II: NAJISTOTNIEJSZE INTERFEJSY PROGRAMISTYCZNE NODE
Rysunek 6.12. Użytkownicy widzą komunikaty pisane przez innych poprzedzone ich pseudonimem
100
WYKOŃCZENIE Kiedy użytkownik się rozłącza, usuwamy go z tablicy users: conn.on('close', function () { count--; delete users[nickname]; });
Dobrym pomysłem będzie też powiadomienie pozostałych użytkowników o opuszczeniu czatu przez daną osobę. Ponieważ po raz kolejny musimy wysłać komunikat do wszystkich czatujących, możemy wydzielić odpowiadający za to fragment kodu: // . . . function broadcast (msg, exceptMyself) { for (var i in users) { if (!exceptMyself || i != nickname) { users[i].write(msg); } } } conn.on('data', function (data) { // . . .
Działania powyższej funkcji nie trzeba wyjaśniać. Teraz możesz zastąpić fragmenty kodu odpowiedzialne za wysyłanie komunikatu do wszystkich nową wygodną funkcją:
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 6: PROTOKÓŁ TCP broadcast('\033[90m > ' + nickname + ' wchodzi do pokoju\033[39m\n'); // . . . broadcast('\033[96m > ' + nickname + ':\033[39m ' + data + '\n', true);
Użyj jej również w metodzie obsługi zdarzenia close (zob. rysunek 6.13): conn.on('close', function () { // . . . broadcast('\033[90m > ' + nickname + ' opuszcza pokój\033[39m\n'); });
101
Rysunek 6.13. Po wyłączeniu okna pierwszego klienta, aby zamknąć połączenie, w oknach pozostałych klientów wyświetla się komunikat pożegnania I to już koniec! Po udanej implementacji serwera TCP zobaczymy teraz, jak zaimplementować klienta TCP w Node.JS. Interfejsy programistyczne klienta będą w dużej mierze podobne do interfejsów niektórych innych klientów, na przykład klientów HTTP usług sieciowych, takich jak Twitter; ich pełne zrozumienie jest zatem ważne.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
CZĘŚĆ II: NAJISTOTNIEJSZE INTERFEJSY PROGRAMISTYCZNE NODE
KLIENT IRC IRC (Internet Relay Chat) jest kolejnym powszechnie używanym protokołem bazującym na TCP. Najczęściej jest on wykorzystywany w aplikacjach podobnych do tej z rysunku 6.14, które pełnią rolę klientów serwerów IRC.
102
Rysunek 6.14. Klient IRC (XChat) w akcji na komputerze z systemem Ubuntu. XChat implementuje protokół IRC poprzez gniazda TCP Ponieważ wcześniej w tym rozdziale zbudowaliśmy serwer TCP, teraz spróbujemy utworzyć klienta TCP. Budowa klienta wiąże się z implementacją protokołu IRC. Oznacza to, że przychodzące i wychodzące dane powinny być zgodne z konwencją, jaką posługują się serwery IRC. Na przykład aby ustawić pseudonim, konieczne jest wysłanie następującego łańcucha znaków: NICK mojnick
IRC nie jest skomplikowanym protokołem. Kilkoma prostymi poleceniami można osiągnąć stosunkowo dużo, na przykład nakłonić do współpracy istniejące aplikacje i serwery (takie jak na rysunku 6.14). W dalszej części rozdziału stworzysz prostego klienta w Node.JS, który pozwoli połączyć się z serwerem, wejść do pokoju i wysłać komunikat.
TWORZYMY MODUŁ Jak zwykle zaczniemy od utworzenia katalogu macierzystego dla naszego projektu oraz pliku package.json:
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 6: PROTOKÓŁ TCP { "name": "klient-irc" , "description": "Nasz pierwszy klient TCP" , "version": "0.0.1" }
Sprawdź, czy wszystko działa, wydając polecenie npm install. Program powinien wyświetlić pusty wiersz2, ponieważ nie zdefiniowaliśmy zależności dla projektu.
INTERFEJS NET.STREAM Moduł net oferuje analogiczną do metody createServer metodę connect, o następującym interfejsie: net.connect(port[[, host], callback]])
Jeżeli podamy w argumencie funkcję, konstrukcja będzie równoznaczna z nasłuchiwaniem zdarzenia connect dla zwracanego obiektu. Kod: var client = net.connect(3000, 'localhost'); client.on('connect', function () {});
jest zatem równoważny: net.connect(3000, 'localhost', function () {});
Znanych z poprzedniego przykładu zdarzeń data oraz close można nasłuchiwać w analogiczny sposób.
IMPLEMENTACJA CZĘŚCI PROTOKOŁU IRC Na początek zainicjalizujemy klienta. Następnie podejmiemy próbę logowania do kanału #node.js serwera irc.freenode.net: var client = net.connect(6667, 'irc.freenode.net')
Najpierw zmień kodowanie na utf-8: client.setEncoding('utf-8')
Kiedy już się połączysz, wybierz i prześlij swój pseudonim. Użyj też polecenia USER, które jest wymagane przez serwery. Wysyłane dane powinny wyglądać tak: NICK mojnick USER mojnick 0 * :realname JOIN #node.js
2
Może też się zdarzyć, że wyświetli ostrzeżenie — przyp. tłum.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
103
CZĘŚĆ II: NAJISTOTNIEJSZE INTERFEJSY PROGRAMISTYCZNE NODE Dlatego w kodzie trzeba je umieścić w następujący sposób: client.on('connect', client.write('NICK client.write('USER client.write('JOIN });
function () { mojnick\r\n'); mojnick 0 * :realname\r\n'); #node.js\r\n')
Zauważ, że każde polecenie kończy się separatorem \r\n. Jest to odpowiednik użycia klawisza Enter w naszym poprzednim przykładzie, w którym korzystaliśmy z Telnetu. Separator \r\n jest też stosowany przez protokół HTTP do oddzielenia wierszy nagłówka.
TEST Z PRAWDZIWYM SERWEREM IRC Uruchom klienta IRC (takiego jak mIRC dla systemu Windows, xChat dla Linuksa lub Colloquy/Linkinus na komputerze mac) i połącz się z: irc.freenode.net #node.js
Teraz uruchom naszego klienta i czekaj, aż mojnick połączy się z kanałem.
104
PODSUMOWANIE W tym rozdziale dokonaliśmy prostej implementacji klienta wykorzystującego moduł net. Przetestowaliśmy go następnie na zewnętrznym serwerze TCP. Jako ćwiczenie nasłuchuj zdarzeń data i spróbuj przetworzyć przychodzące dane, tak aby móc na ich podstawie wyświetlać własne komunikaty na kanale #node.js. Łącząc to z istniejącym kodem, otrzymasz bota odpowiadającego automatycznie na polecenia. Jeśli ktoś na przykład wpisze słowo „date” (wykryjesz je w zdarzeniach data), będziesz mógł wyświetlić wynik wywołania new Date(). W następnym rozdziale poznasz możliwości protokołu HTTP, któremu to Node.JS zawdzięcza w dużej mierze swoją sławę. Masz już solidną wiedzę o podstawach, a znajomość protokołu HTTP, który w modelu sieciowym znajduje się o warstwę wyżej niż TCP, pozwoli Ci na prawdziwy wgląd w istotę działania Node.JS.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
7 ROZDZIAŁ
PODRĘCZNIK NODE.JS
PROTOKÓŁ HTTP
HTTP (Hypertext Transfer Protocol) jest protokołem napędzającym całą sieć WWW. Jak wspomnieliśmy w rozdziale 6., pracuje on w warstwie aplikacji, powyżej stosu TCP. Ten rozdział przybliża działanie interfejsów programistycznych serwera i klienta modułu HTTP Node.JS. Są one dosyć proste w obsłudze, ale poznasz również ich wady, które ujawniają się w praktyce przy tworzeniu za ich pomocą stron i aplikacji sieciowych. Aby wyeliminować te wady, przedstawimy w następnych rozdziałach pewne warstwy abstrakcji
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ponad serwerem HTTP, wprowadzające komponenty wielokrotnego użytku. Pamiętaj, że ponieważ ten sam kod odpowiada zarówno za serwer, jak i za samą stronę, po każdej modyfikacji kodu musisz uruchomić ponownie proces Node, który go zasila, aby zmiany były widoczne. Na końcu rozdziału poznasz narzędzie, które bardzo usprawnia ten proces. Na samym początku przyjrzyjmy się budowie protokołu HTTP.
CZĘŚĆ II: NAJISTOTNIEJSZE INTERFEJSY PROGRAMISTYCZNE NODE
STRUKTURA HTTP Kluczowymi dla struktury protokołu pojęciami są żądania (ang. requests) oraz odpowiedzi (ang. responses), którym w Node.JS odpowiadają konstruktory http.ServerRequest i http.ServerResponse. Kiedy użytkownik nawiguje do strony, aplikacja użytkownika (przeglądarka) tworzy żądanie, które jest następnie przesyłane do serwera WWW za pomocą protokołu TCP, po czym serwer generuje odpowiedź. Jak wyglądają żądania i odpowiedzi? Dowiedzmy się, tworząc serwer HTTP Node wyświetlający komunikat „Witaj świecie” i nasłuchujący na porcie http://localhost:3000: require('http').createServer(function (req, res) { res.writeHead(200); res.end('Witaj świecie'); }).listen(3000);
Nawiąż teraz połączenie telnet i wpisz żądanie w postaci: GET / HTTP/1.1
106
Po wprowadzeniu GET / HTTP/1.1 potwierdź żądanie, wciskając dwukrotnie klawisz Enter. Odpowiedź pojawi się natychmiast! Pokazano ją na rysunku 7.1.
Rysunek 7.1. Odpowiedź wygenerowana przez nasz serwer HTTP Tekst odpowiedzi wygląda następująco: HTTP/1.1 200 OK Connection: keep-alive Transfer-Encoding: chunked e Witaj świecie 0
Pierwszą ważną część odpowiedzi stanowią nagłówki.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 7: PROTOKÓŁ HTTP
NAGŁÓWKI Jak widać, protokół HTTP działa na podobnej zasadzie, co protokół IRC. Jego celem jest umożliwienie wymiany dokumentów. Wykorzystuje nagłówki (ang. headers) poprzedzające żądanie i odpowiedź, które opisują różne aspekty komunikacji i przesyłanej zawartości. Jako przykład pomyśl o różnych typach zawartości dostarczanych przez strony WWW: tekście, HTML, XML, JSON, PNG czy obrazach JPEG i wielu innych. Typ przesyłanej zawartości określany jest w doskonale nam znanym nagłówku Content-Type. Zobaczmy, jak to wygląda w praktyce. Wróć do przykładu „Witaj świecie”, ale tym razem dodaj trochę kodu HTML: require('http').createServer(function (req, res) { res.writeHead(200); res.end('Witaj świecie'); }).listen(3000);
Zwróć uwagę, że wokół słowa „świecie” umieściliśmy znacznik pogrubiający tekst. Efekt końcowy możesz sprawdzić, korzystając ponownie z prostego klienta TCP (zob. rysunek 7.2).
107
Rysunek 7.2. Odpowiedź Witaj świecie Treść odpowiedzi niczym nie zaskakuje: GET / HTTP/1.1 HTTP/1.1 200 OK Connection: keep-alive Transfer-Encoding: chunked 15 Witaj świecie 0
Spójrz teraz jednak na efekt uruchomienia przykładu w przeglądarce (zob. rysunek 7.3).
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
CZĘŚĆ II: NAJISTOTNIEJSZE INTERFEJSY PROGRAMISTYCZNE NODE
Rysunek 7.3. Przeglądarka wyświetla odpowiedź jako zwykły tekst Tekst nie został sformatowany, a znaki diakrytyczne są wyświetlone nieprawidłowo. Dlaczego? Jak się okazuje, klient HTTP (przeglądarka) nie wie, jaki jest typ przesyłanej zawartości, ponieważ nie dostarczyliśmy mu takiej informacji w trakcie komunikacji. Przyjmuje on więc domyślny typ zawartości text/plain, czyli zwykły tekst, i nie próbuje jej interpretować jako kodu HTML.
108
Umieszczenie w naszym kodzie odpowiedniego nagłówka rozwiązuje ten problem (zob. rysunek 7.4): require('http').createServer(function (req, res) { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end('Witaj świecie'); }).listen(3000);
Rysunek 7.4. Odpowiedź; tym razem z odpowiednim nagłówkiem Tekst odpowiedzi jest następujący: HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 Connection: keep-alive
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 7: PROTOKÓŁ HTTP Transfer-Encoding: chunked 15 Witaj świecie 0
Zwróć uwagę, że nagłówek został dołączony jako część tekstu odpowiedzi. Odpowiedź jest teraz poprawnie interpretowana przez przeglądarkę, co widać na rysunku 7.5.
Rysunek 7.5. Słowo „świecie” zostało pogrubione przez przeglądarkę Zauważ, że pomimo określenia tylko jednego nagłówka w metodzie writeHead, Node dołącza jeszcze dwa inne nagłówki: Transfer-Encoding oraz Connection1. Domyślną wartością Transfer-Encoding jest chunked2. Jest to podyktowane głównie asynchronicznym charakterem Node, w którym stopniowe budowanie odpowiedzi nie należy do rzadkości. Rozważmy następujący przykład: require('http').createServer(function (req, res) { res.writeHead(200); res.write('Witaj'); setTimeout(function () { res.end('świecie'); }, 500); }).listen(3000);
1
Transfer-Encoding wyznacza sposób przesłania zawartości, Connection określa natomiast, czy połączenie będzie utrzymywane po zakończeniu żądania — przyp. tłum.
2
Wartość chunked oznacza, że zawartość będzie przesyłana pakietami — przyp. tłum.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
109
CZĘŚĆ II: NAJISTOTNIEJSZE INTERFEJSY PROGRAMISTYCZNE NODE Zauważ, że dane możesz również wysłać za pomocą wielokrotnych wywołań metody write, zanim wywołasz metodę end. Starając się, by odpowiedź dotarła do klienta tak szybko, jak to tylko możliwe, Node wysyła nagłówki odpowiedzi i pierwszy pakiet danych (Witaj) już w momencie pierwszego wywołania write. Kolejny pakiet jest wysyłany dopiero po wywołaniu funkcji zwrotnej z metody setTimeout. Ponieważ tym razem korzystamy z wywołania end zamiast write, Node finalizuje odpowiedź i żadne dane nie mogą w niej już zostać później zapisane. Innym przykładem efektywności zapisu danych w pakietach są operacje na systemie plików. Nierzadko zadanie serwera WWW polega na zwróceniu pliku (na przykład obrazu) znajdującego się gdzieś na dysku twardym. Jako że Node pozwala na zapis w odpowiedzi w pakietach, a także umożliwia odczyt pliku analogiczną metodą, możemy do tego celu wykorzystać interfejs ReadStream. Poniższy kod odczytuje obrazek image.png i zwraca go z odpowiednim nagłówkiem Content-Type:
110
require('http').createServer(function (req, res) { res.writeHead(200, { 'Content-Type': 'image/png'); var stream = require('fs').createReadStream('image.png'); stream.on('data', function (data) { res.write(data); }); stream.on('end', function () { res.end(); }); }).listen(3000);
Zapisując obraz jako serię pakietów, zapewniasz:
Efektywny przydział pamięci. Odczyt całego obrazka przed zapisem (przy użyciu fs.readFile) prawdopodobnie spowodowałby w dłuższym okresie i przy dużej liczbie żądań większe zużycie pamięci.
Natychmiastowy zapis dostępnych danych.
Ponadto, przekształcasz potokowo jeden strumień (FS) w drugi (obiekt http.ServerResponse). Jak już wspomniałem, strumienie są bardzo ważnymi konstrukcjami w Node.JS. Przetwarzanie potokowe (ang. piping) strumieni to często wykonywana operacja, dlatego Node.JS oferuje metodę, dzięki której powyższy przykład może być zapisany krócej: require('http').createServer(function (req, res) { res.writeHead(200, { 'Content-Type': 'image/png'); require('fs').createReadStream('image.png').pipe(res); }).listen(3000);
Teraz, kiedy już wiesz, dlaczego domyślną wartością Transfer-Encoding jest chunked, możemy omówić połączenia.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 7: PROTOKÓŁ HTTP
POŁĄCZENIA Jeżeli porównasz implementacje serwerów TCP i HTTP, okaże się, że są podobne. W obu wywołujemy metodę createServer i obie bazują na wywołaniu zwrotnym po połączeniu klienta. Podstawową różnicą jest jednak obiekt przekazywany w funkcji zwrotnej. W przypadku serwera net będzie to obiekt połączenia, a w przypadku serwera HTTP — obiekty żądania i odpowiedzi. Są dwa powody tego stanu rzeczy. Po pierwsze, serwer HTTP jest interfejsem programistycznym wyższego rzędu, który oferuje narzędzia do obsługi konkretnych zestawów funkcji i zachowań, typowych dla protokołu HTTP. Przyjrzyjmy się na przykład właściwości headers obiektu żądania (w przykładzie jest nim parametr req) przy próbie dostępu do serwera przez przeglądarkę (zob. rysunek 7.6). Wyświetlmy wartość req.headers za pomocą polecenia console.log: require('http').createServer(function (req, res) { console.log(req.headers); res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end('Witaj świecie'); }).listen(3000);
Rysunek 7.6. Właściwość headers obiektu ServerRequest wyświetlona przy pomocy console.log Warto zauważyć, że Node oszczędza nam tu dużo niepotrzebnej pracy. Pobiera informację z przeglądarki, interpretuje ją i konstruuje wygodny w użyciu obiekt JavaScript. Właściwości obiektu są zapisane małymi literami, nie trzeba więc nawet pamiętać, czy nazywały się Content-type, Content-Type czy Content-TYPE. Drugim i nawet ważniejszym powodem jest fakt, iż przeglądarki internetowe nie korzystają z więcej niż jednego połączenia podczas komunikacji ze stroną. Współczesne przeglądarki mogą otwierać do ośmiu niezależnych połączeń do jednego serwera i wysyłać żądania za pomocą każdego z nich, aby przyspieszyć ładowanie strony.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
111
CZĘŚĆ II: NAJISTOTNIEJSZE INTERFEJSY PROGRAMISTYCZNE NODE Node pozwala zapomnieć o połączeniach i skoncentrować się na żądaniach. Dlatego choć poprzez właściwość req.connection można uzyskać dostęp do połączenia TCP, najczęściej będziemy mieli styczność z abstrakcjami żądań i odpowiedzi. Domyślnie, Node nakazuje przeglądarkom utrzymywanie połączenia i wysyłanie za jego pomocą kolejnych żądań. Informuje je o tym wartość keep-alive wspomnianego wcześniej nagłówka Connection. Na ogół jest to zachowanie pożądane w kontekście wydajności (przeglądarki nie tracą czasu na zamykanie i ponowne nawiązywanie połączeń TCP), ale można również nadpisać nagłówek, przekazując inną wartość w metodzie writeHead, na przykład Close. W kolejnym przykładzie rozwiążemy, z pomocą interfejsów modułu http Node, rzeczywiste zadanie; podejmiemy mianowicie próbę przetworzenia formularza wysyłanego przez użytkownika.
PROSTY SERWER WWW W ramach tego przykładu wykorzystamy niektóre z opisanych wyżej elementów, na przykład nagłówek Content-Type.
112
Dowiemy się także, w jaki sposób przeglądarki internetowe wymieniają zaszyfrowane dane jako część wysyłanego formularza oraz jak mogą one zostać sparsowane do konstrukcji JavaScript.
TWORZYMY MODUŁ Jak zwykle zaczniemy od utworzenia katalogu macierzystego dla naszego projektu oraz pliku package.json: { "name": "formularz-http" , "description": "Serwer HTTP przetwarzający formularze" , "version": "0.0.1" }
Sprawdź, czy wszystko działa, wydając polecenie npm install. Program powinien wyświetlić pusty wiersz3, ponieważ nie zdefiniowaliśmy zależności dla projektu.
WYŚWIETLAMY FORMULARZ Tak jak w przykładzie Witaj świecie, wyświetlimy trochę kodu HTML. W tym przypadku będzie on reprezentował formularz. Umieść następującą zawartość w pliku server.js:
3
Może też się zdarzyć, że wyświetli ostrzeżenie — przyp. tłum.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 7: PROTOKÓŁ HTTP require('http').createServer(function (req, res) { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end([ '' , 'Mój formularz' , '' , 'Dane użytkownika' , 'Jak masz na imię?' , '' , 'Wyślij' , '' ].join('')); }).listen(3000);
W celu zachowania większej przejrzystości tekst odpowiedzi zorganizowałem w tablicę, która łączona jest w jeden łańcuch za pomocą metody join. Poza tym, przykład nie różni się specjalnie od przykładu Witaj świecie. Zwróć uwagę na wartości atrybutów znacznika . Docelowym adresem URL jest w tym przypadku /url, a metodą HTTP — POST. Zauważ też, że atrybut name pola, w którym użytkownik wprowadza dane, przyjmuje wartość name. Uruchom teraz serwer: $ node server.js
Następnie przejdź do przeglądarki i wpisz adres serwera, jak na rysunku 7.7, aby zobaczyć wyświetlony formularz dla kodu HTML:
Rysunek 7.7. Tak powinna wyglądać wyświetlona strona z formularzem Możesz spróbować wysłać formularz klawiszem Enter. Przeglądarka utworzy wtedy nowe żądanie (będzie ono zawierać dane), ale ponieważ jedynym zadaniem naszej aplikacji jest obecnie wyświetlenie kodu formularza, po wciśnięciu klawisza Enter strona będzie wyglądać tak samo (zob. rysunek 7.8). Wpisz imię i kliknij Wyślij.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
113
CZĘŚĆ II: NAJISTOTNIEJSZE INTERFEJSY PROGRAMISTYCZNE NODE
Rysunek 7.8. Przykład przesyłania formularza W wyniku wysłania formularza zmienił się adres URL, ale odpowiedź pozostała taka sama, co widać na rysunku 7.9.
114
Rysunek 7.9. Pomimo wysłania formularza Node obsłuży żądanie w taki sam sposób, zwracany kod HTML pozostanie więc niezmieniony Aby Node potraktował żądanie obsługi formularza inaczej i wygenerował stosowną odpowiedź, musimy rozpoznać metodę żądania oraz jego adres URL.
METODY I ADRESY URL Nie ulega wątpliwości, że po wciśnięciu przez użytkownika klawisza Enter przeglądarka powinna wyświetlić coś innego. Dane z formularza powinny zostać przetworzone. W tym celu zbadamy wartość właściwości url obiektu żądania. Kod w pliku server.js powinien na tym etapie wyglądać następująco:
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 7: PROTOKÓŁ HTTP require('http').createServer(function (req, res) { if ('/' == req.url) { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end([ '' , 'Mój formularz' , '' , 'Dane użytkownika' , 'Jak masz na imię?' , '' , 'Wyślij' , '' ].join('')); } else if ('/url' == req.url) { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end('Wysłałeś żądanie ' + req.method + ''); } }).listen(3000);
Jeśli przejdziesz teraz do adresu /, jak pokazano na rysunku 7.10, nic się nie zmieni.
115
Rysunek 7.10. Kod obsługujący żądanie nadal pokazuje tę samą zawartość HTML po przejściu do bazowego adresu URL Wpisując adres /url, zobaczysz stronę z rysunku 7.11. Podany adres URL jest identyczny z adresem req.url z instrukcji else if, w wyniku czego generowana jest stosowna odpowiedź. Jeśli jednak wprowadzisz swoje imię w polu formularza, zobaczysz wiadomość pokazaną na rysunku 7.12. Stanie się tak, ponieważ przeglądarka wyśle dane formularza za pomocą metody HTTP określonej w atrybucie action znacznika . Wartością req.method będzie w tym przypadku POST i wygenerowana zostanie odpowiedź widoczna na rysunku 7.12. Mamy tu, jak widać, do czynienia z dwoma zmiennymi określającymi żądanie: adresem URL i metodą.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
CZĘŚĆ II: NAJISTOTNIEJSZE INTERFEJSY PROGRAMISTYCZNE NODE
Rysunek 7.11. Efekt przejścia do adresu /url pojawiający się w rezultacie zmiany req.url
116
Rysunek 7.12. Tutaj req.method ma wartość POST Node.JS umieszcza w zmiennej url wszystko, co następuje po adresie serwera. Jeśli przejdziesz do strony http://myhost.com/url?this+is+a+long+url, zawartością url będzie: /url?this+is+a+long+url. Fundamentalny protokół w internecie, HTTP/1.1 (który pamiętamy między innymi z przykładu wykorzystującego Telnet w rozdziale 6.), określa cztery różne metody żądania:
GET (domyślna),
POST,
PUT,
DELETE,
PATCH (najnowsza).
Zasadą, która przyświeca temu podziałowi, jest zmiana zasobu pod adresem URL na serwerze, dokonywana przez klienta HTTP za pomocą jednej z metod i przy użyciu określonych danych stanowiących ciało żądania.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 7: PROTOKÓŁ HTTP
DANE Wysyłając kod HTML w odpowiedzi, trzeba określić nagłówek Content-Type oraz ciało odpowiedzi. Podobnie jak odpowiedź, również żądanie składa się z nagłówka Content-Type i ciała. Do efektywnego przetwarzania formularzy informacja o tych dwóch elementach jest niezbędna. Podobnie jak przeglądarka nie wie, czy Witaj świecie to kod HTML czy zwykły tekst, dopóki nie zostanie to jawnie określone, serwer Node musi zostać również poinformowany o tym, czy użytkownik przesyła swoje imię w formacie JSON, XML czy jako zwykły tekst. Kod w pliku server.js powinien wyglądać teraz tak: require('http').createServer(function (req, res) { if ('/' == req.url) { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end([ '' , 'Mój formularz' , '' , 'Dane użytkownika' , 'Jak masz na imię?' , '' , 'Wyślij' , '' ].join('')); } else if ('/url' == req.url && 'POST' == req.method) { var body = ''; req.on('data', function (chunk) { body += chunk; }); req.on('end', function () { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end('Typ zawartości: ' + req.headers['content-type'] + '' + 'Dane:' + body + ''); }); } }).listen(3000);
Co się tu dzieje? Nasłuchujemy zdarzeń data oraz end. Tworzymy łańcuch body, który następnie wypełniamy kolejnymi pakietami danych, co trwa aż do momentu wygenerowania zdarzenia end. Jest to możliwe, ponieważ Node.JS pozwala na przetwarzanie danych, gdy tylko dotrą na serwer. Jako że dane nadchodzą w pakietach TCP, w praktyce zdarza się, że najpierw otrzymujemy pewien fragment danych, a dopiero po jakimś czasie resztę. Wyślij formularz ponownie i spójrz na odpowiedź z rysunku 7.13.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
117
CZĘŚĆ II: NAJISTOTNIEJSZE INTERFEJSY PROGRAMISTYCZNE NODE
Rysunek 7.13. W tym przykładzie wyświetlamy typ zawartości i dane żądania Podczas wyszukiwania w Google na przykład adres URL może wyglądać tak, jak na rysunku 7.14.
118
Rysunek 7.14. Zaznaczona część adresu URL przy wyszukiwaniu ma postać q= Zwróć uwagę na słowo urlencoded w łańcuchu typu zawartości. Adres URL jest kodowany (ang. encoded) w taki sam sposób, jak zawartość formularza. Ten fragment adresu URL nazywamy łańcuchem zapytania (ang. query string). Node.JS udostępnia moduł querystring, który pomaga parsować te łańcuchy do postaci łatwo dostępnych danych, podobnie jak to miało miejsce przy nagłówkach. Utwórz plik qs-example.js z następującą zawartością i uruchom go (zob. rysunek 7.15). console.log(require('querystring').parse('name=Guillermo')); console.log(require('querystring').parse('q=guillermo+rauch'));
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 7: PROTOKÓŁ HTTP
Rysunek 7.15. Wynik wywołań metody parse Jak łatwo zauważyć, moduł querystring potrafi na podstawie łańcucha stworzyć strukturę danych obiektu. Jest to proces analogiczny do parsowania nagłówków z żądania HTTP do postaci wygodnego obiektu headers. Z modułu tego będziemy korzystać, aby uzyskać łatwy dostęp do pola wysyłanego wraz z formularzem.
SKŁADAMY ELEMENTY W CAŁOŚĆ Jesteśmy już gotowi przetworzyć dane formularza i wyświetlić je użytkownikowi. Plik server.js powinien teraz zawierać zaprezentowany poniżej kod. Zauważ, że w metodzie obsługi zdarzenia end korzystamy z modułu querystring, pobierając wartość name z otrzymanego obiektu, aby wyświetlić ją użytkownikowi. Pamiętaj, że właściwość name obiektu odpowiada atrybutowi name zdefiniowanego w kodzie HTML znacznika . Na tym etapie kod w pliku server.js wygląda następująco: var qs = require('querystring'); require('http').createServer(function (req, res) { if ('/' == req.url) { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end([ '' , 'Mój formularz' , '' , 'Dane użytkownika' , 'Jak masz na imię?' , '' , 'Wyślij' , '' ].join('')); } else if ('/url' == req.url && 'POST' == req.method) { var body = ''; req.on('data', function (chunk) { body += chunk; }); req.on('end', function () { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end('Twoje imię to ' + qs.parse(body).name + '');
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
119
CZĘŚĆ II: NAJISTOTNIEJSZE INTERFEJSY PROGRAMISTYCZNE NODE }); } }).listen(3000);
Wpisz teraz odpowiedni adres w przeglądarce i voilà! Efekt powinien być taki, jak na rysunku 7.16.
Rysunek 7.16. Wartość wprowadzona w polu zostaje wyświetlona
120
DOPRACOWANIE SZCZEGÓŁÓW Nierozwiązany pozostaje pewien problem: co jeśli adres URL nie odpowiada żadnemu z określonych w wyrażeniach warunkowych adresów? Po przejściu przez użytkownika do adresu /test serwer nie odpowiada, a aplikacja użytkownika (przeglądarka) zawiesza się. Aby rozwiązać ten problem, należy odesłać kod odpowiedzi HTTP 404 (Not Found), jeżeli serwer nie wie, jak ma potraktować żądanie. Dodajmy zatem w pliku server.js blok else, w którym użyjemy polecenia writeHead z kodem odpowiedzi 404: var qs = require('querystring'); require('http').createServer(function (req, res) { if ('/' == req.url) { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end([ '' , 'Mój formularz' , '' , 'Dane użytkownika' , 'Jak masz na imię?' , '' , 'Wyślij' , '' ].join('')); } else if ('/url' == req.url && 'POST' == req.method) { var body = '';
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 7: PROTOKÓŁ HTTP req.on('data', function (chunk) { body += chunk; }); req.on('end', function () { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end('Twoje imię to ' + qs.parse(body).name + ''); }); } else { res.writeHead(404); res.end('Nie znaleziono'); } }).listen(3000);
Twój pierwszy serwer HTTP jest już gotowy! Kod nieidealny, ale w kolejnych rozdziałach nauczysz się tworzyć złożone serwery HTTP w bardziej efektywny sposób. Teraz natomiast czas się zająć odpowiednikiem interfejsu programistycznego serwera: klientem HTTP.
KLIENT TWITTERA Umiejętność wysyłania żądań do innych serwerów WWW z poziomu Node.JS jest nie do przecenienia. Rola protokołu HTTP nie ogranicza się wyłącznie do wymiany znaczników, ich wyświetlenia oraz prezentowania użytkownikowi (dzięki HTML). Polega ona również na transmisji danych pomiędzy serwerami znajdującymi się w różnych sieciach. Wykorzystywany do tego format JSON szybko staje się faktycznym standardem, co sprzyja umocnieniu pozycji Node.JS jako preferowanego rozwiązania po stronie serwera, ponieważ notacja JSON powstała na bazie składni literału obiektu JavaScript. W tym przykładzie zobaczysz, jak wysłać zapytanie do interfejsu programistycznego Twittera, przekształcić otrzymany w odpowiedzi kod JSON w łatwe do iteracyjnego przetworzenia struktury danych i wygenerować przyjazne dla użytkownika dane wyjściowe.
TWORZYMY MODUŁ Jak zwykle zaczniemy od utworzenia katalogu macierzystego dla naszego projektu oraz pliku package.json: { "name": "klient-twittera" , "description": "Klient HTTP Twittera" , "version": "0.0.1" }
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
121
CZĘŚĆ II: NAJISTOTNIEJSZE INTERFEJSY PROGRAMISTYCZNE NODE
WYSYŁANIE PROSTEGO ŻĄDANIA HTTP Podobnie jak przy stworzonym przez nas kliencie TCP (i nieprzypadkowo), obiekt klienta możemy zainicjalizować za pomocą wywołania statycznej metody request modułu http. Zilustrujmy to przykładem. Na początek wróćmy do typowego serwera HTTP: require('http').createServer(function (req, res) { res.writeHead(200); res.end('Witaj świecie'); }).listen(3000);
Następnie stworzymy klienta, który uzyska odpowiedź i wyświetli ją przy użyciu kolorów w konsoli:
122
require('http').request({ host: '127.0.0.1' , port: 3000 , url: '/' , method: 'GET' }, function (res) { var body = ''; res.setEncoding('utf8'); res.on('data', function (chunk) { body += chunk; }); res.on('end', function () { console.log('\n Otrzymaliśmy odpowiedź: \033[96m' + body + '\033[39m\n'); }); }).end();
Pierwszą instrukcją jest wywołanie metody request. Inicjalizuje ona nowy obiekt http.Client.Request. Zauważ, że zwracana treść jest tworzona stopniowo, podobnie jak miało to miejsce w przypadku żądań wysyłanych z przeglądarki internetowej w poprzednim przykładzie z tego rozdziału. Zdalny serwer, z którym się łączymy, może wysyłać dane w pakietach, które trzeba połączyć, aby uzyskać pełną treść odpowiedzi. Może się też zdarzyć, że wszystkie dane nadejdą w ramach jednego zdarzenia data, ale nigdy nie jest to pewne. Dlatego w tym przypadku, aby wyświetlić treść odpowiedzi w konsoli, należy nasłuchiwać zdarzenia end. Oprócz tego, ustawiamy też domyślne kodowanie odpowiedzi na utf8 za pomocą metody setEncoding, ponieważ w konsoli chcemy wyświetlić tylko tekst. Pobranie obrazu PNG i próba wyświetlenia go za pomocą klienta jako utf8 nie byłaby na przykład najlepszym pomysłem.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 7: PROTOKÓŁ HTTP Uruchom teraz serwer, a następnie klienta (zob. rysunek 7.17): $ node client
Rysunek 7.17. Odpowiedź Witaj świecie z naszego serwera jest wyświetlana po jej zażądaniu przez klienta W następnym punkcie pokażemy, jak wraz z żądaniem wysłać dane.
WYSŁANIE DANYCH Zauważ, że po wywołaniu metody request w poprzednim przykładzie musieliśmy też wywołać metodę end. Jest to spowodowane tym, że po utworzeniu żądania można dokonywać operacji na obiekcie request przed jego wysłaniem serwer. Następny przykład, w którym spróbujemy wysłać dane na serwer, bardzo dobrze to zilustruje. Czy pamiętasz formularz w przeglądarce z poprzedniego przykładu? Spróbujemy podobnego rozwiązania z tą różnicą, że klienta utworzymy w Node, a zamiast formularza użyjemy standardowego strumienia wejścia (stdin), korzystając z wiedzy zdobytej w rozdziale 5. Serwer przetwarza dane: var qs = require('querystring'); require('http').createServer(function (req, res) { var body = ''; req.on('data', function (chunk) { body += chunk; }); req.on('end', function () { res.writeHead(200); res.end('Gotowe'); console.log('\n odebrano imię \033[90m' + qs.parse(body).name + '\033[39m\n'); }); }).listen(3000);
Działanie klienta jest odwrotne. Używając metody stringify modułu querystring, można przekształcić obiekt w dane zakodowane w adresie URL:
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
123
CZĘŚĆ II: NAJISTOTNIEJSZE INTERFEJSY PROGRAMISTYCZNE NODE var http = require('http') , qs = require('querystring') function send (theName) { http.request({ host: '127.0.0.1' , port: 3000 , url: '/' , method: 'POST' }, function (res) { res.setEncoding('utf8'); res.on('end', function () { console.log('\n \033[90m żądanie wysłane!\033[39m'); process.stdout.write('\n Twoje imię: '); }); }).end(qs.stringify({ name: theName })); } process.stdout.write('\n Twoje imię: '); process.stdin.resume(); process.stdin.setEncoding('utf-8'); process.stdin.on('data', function (name) { send(name.replace('\n', '')); });
124
Zauważ, że dane są przekazywane do metody end w taki sam sposób, jak przy tworzeniu odpowiedzi na serwerze. W tym przypadku nie musimy się martwić o dane otrzymywane z serwera. Wiemy po prostu, że po wywołaniu metody end możemy wyświetlić cały tekst żądania i poprosić ponownie użytkownika o wprowadzenie danych. Na rysunku 7.18 pokazano działanie obu stron komunikacji. Na rysunku w górnej części serwer wyświetla imię, które zostało wysłane za pomocą formularza na rysunku w dolnej części. Wysyłanie danych wraz z żądaniem zamyka listę rzeczy, które powinieneś wiedzieć o interfejsie request. Wróćmy teraz do naszego głównego zadania.
POBIERANIE TWEETÓW Jesteśmy już gotowi na zastosowanie protokołu HTTP w praktyce! Stworzymy polecenie tweets, które będzie przyjmowało w argumencie kryterium wyszukiwania i wyświetli najnowsze tweety na dany temat. Zaglądając do dokumentacji interfejsu programistycznego Twittera, a dokładniej do jej części odpowiedzialnej za wyszukiwanie publiczne, zobaczysz, że adresy URL mają tam formę: https://api.twitter.com/1.1/search/tweets.json?q=blue+angels. Z dokumentacji wynika też, że najnowsze API Twittera obsługuje wyłącznie żądania przesyłane jako HTTPS,
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 7: PROTOKÓŁ HTTP
125 Rysunek 7.18. Po wprowadzeniu imienia w formularzu w drugim oknie i zatwierdzeniu Enterem zostaje ono prawidłowo przetworzone przez serwer w pierwszym oknie a więc żądania HTTP w bezpiecznym połączeniu SSL; ponadto, każde żądanie musi być autoryzowane co najmniej kluczem dostępowym aplikacji (w przypadku żądań niedotyczących kontekstu użytkownika; wyszukiwanie tweetów zalicza się właśnie do tej kategorii). Aby przeszukać tweety, trzeba więc w ramach swojego konta Twitter zarejestrować aplikację (https://dev.twitter.com/apps); rejestracja jest stosunkowo prosta, bo ogranicza się do podania podstawowych danych o aplikacji, takich jak nazwa czy adres strony domowej aplikacji (w przypadku aplikacji testowych może to być adres fikcyjny). Każda zarejestrowana aplikacja otrzymuje komplet danych uwierzytelniających, których można użyć do wygenerowania rozmaitych kluczy dostępowych. W przypadku klucza aplikacyjnego potrzebna jest para kluczy klienckich, tzw. „consumer key” i „consumer secret”; na ich podstawie można za pomocą odpowiedniego żądania do interfejsu autoryzacyjnego Twittera wygenerować klucz dostępowy aplikacji, przeznaczony do stosowania w aplikacjach korzystających z żądań niezwiązanych z kontem użytkownika. Sposób pozyskania tego klucza jest dokładnie opisany w dokumentacji interfejsu programistycznego Twittera (https://dev.twitter.com/docs/auth/application-only-auth); dla uproszczenia, w plikach kodu źródłowego dołączonych do książki znajduje się skrypt Node.JS accesstoken.js, który wywołany z dwoma argumentami (odpowiednio: klucz kliencki i sekret kliencki) wypisuje na wyjściu wygenerowany przez Twittera klucz dostępowy. Tak wygenerowany klucz nie ma terminu ważności, działa aż do odwołania przez właściciela aplikacji.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
CZĘŚĆ II: NAJISTOTNIEJSZE INTERFEJSY PROGRAMISTYCZNE NODE Klucz dostępowy aplikacji należy dołączać do każdego żądania HTTPS w polu nagłówka Authorization (Authorization: Bearer KLUCZ-DOSTĘPOWY-APLIKACJI). Podsumowując, żądanie HTTP (HTTPS) do wyszukiwarki tweetów musi zawierać URL z ciągiem zapytania oraz odpowiedni nagłówek Authorization. Wyniki wyglądają natomiast tak (zauważ, że pominąłem pierwszy fragment odpowiedzi, z listą znalezionych tweetów): { "statuses": [ { // właściwe dane tweetów... } ], "search_metadata": { "completed_in":0.031, "max_id":369561364513439740, "max_id_str":"369561364513439740",
126
"next_results":"?max_id=369561364081414143&q=blue%20angels&include_entities=1",", "query":"blue+angels", "refresh_url":"?since_id=369561364513439745&q=blue%20angels&include_entities=1", "count":15, "since_id":0, "since_id_str":0 } } „[ { // …
Użyte struktury danych nie zaskakują: kryterium wyszukiwania jest i tym razem zakodowane w adresie URL (q=blue+angels), a wyniki są również zwracane w formacie JSON. Same tweety znajdują się w tablicy results obiektu odpowiedzi. Ponieważ tworzymy polecenie, które będzie przyjmować argument, podobnie jak w rozdziale 5., skorzystamy z elementów tablicy argv. Za pomocą modułu querystring skonstruujemy adres URL, dołączymy do żądania nagłówek autoryzacji, a następnie uzyskamy dane odpowiedzi. Metodą dostępu do zasobu będzie oczywiście GET, a użytym portem — 443; obie wartości są domyślne, można je więc bez konsekwencji pominąć (w tym pierwszym przykładzie klienta HTTPS w celu zachowania przejrzystości uwzględniłem jeszcze wartość GET). var , var var
qs = require('querystring') https = require('https'); auth = 'KLUCZ-DOSTĘPOWY-APLIKACJI'; // patrz skrypt accesstoken.js search = process.argv.slice(2).join(' ').trim();
if (!search.length) { return console.log('\n Użycie: node tweets \n'); } console.log('\n wyszukiwana fraza: \033[96m' + search + '\033[39m\n'); https.request({ host: api.twitter.com', path: '/1.1/search/tweets.json?' + qs.stringify({ q: search }), headers: {'Authorization': 'Bearer ' + auth}
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 7: PROTOKÓŁ HTTP }, function (res) { var body = ''; res.setEncoding('utf8'); res.on('data', function (chunk) { body += chunk; }); res.on('end', function () { var obj = JSON.parse(body); obj.statuses.forEach(function (tweet) { console.log(' \033[90m' + tweet.text + '\033[39m'); console.log(' \033[94m' + tweet.user.screen_name + '\033[39m'); console.log('--'); }); }); }).end()
Powyższy kod sprawdzi poprawność składni; jeśli nie znajdzie w tablicy process.argv kryterium wyszukiwania, wyświetli tekst pomocy (zob. rysunek 7.19).
127
Rysunek 7.19. Użycie polecenia bez argumentu powoduje wyświetlenie pomocy Jeśli podasz argumenty, przeprowadzone zostanie wyszukiwanie, co pokazano na rysunku 7.20. Twitter zwraca odpowiedź w formacie JSON, którą parsujemy przy użyciu pętli w metodzie obsługi zdarzenia end, a następnie wyświetlamy użytkownikowi.
Rysunek 7.20. Wprowadzona fraza spowodowała wyświetlenie kilku interesujących tweetów
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
CZĘŚĆ II: NAJISTOTNIEJSZE INTERFEJSY PROGRAMISTYCZNE NODE Do tej pory bardzo często posługiwaliśmy się metodą http.request (tudzież zabezpieczoną odmianą https.request). Wysyłaliśmy głównie żądania GET, które w gruncie rzeczy są najczęściej spotykane. Usługi sieciowe z reguły udostępniają więcej punktów końcowych GET niż POST czy PUT. Wysyłanie danych (ciała żądania) wraz z żądaniem też zdarza się dosyć rzadko. Node.JS wychodzi naprzeciw potrzebom, udostępniając metodę request.get. Część kodu odpowiedzialną za wywołanie interfejsu programistycznego Twittera (za pomocą https.request) można by przepisać w ten sposób:
128
http.get({ host: 'api.twitter.com', path: '/1.1/search/tweets.json?' + qs.stringify({ q: search }), headers: { 'Authorization': 'Bearer ' + auth } }, function (res) { var body = ''; res.setEncoding('utf8'); res.on('data', function (chunk) { body += chunk; }); res.on('end', function () { var obj = JSON.parse(body); obj.statuses.forEach(function (tweet) { console.log(' \033[90m' + tweet.text + '\033[39m'); console.log(' \033[94m' + tweet.user.screen_name + '\033[39m'); console.log('--'); }); }); })
Jedyna różnica polega na tym, że nie musimy wywoływać metody end. Zwiększa się też trochę czytelność kodu. Metoda przyjmuje parametr o domyślnej wartości GET, nie musimy go zatem dodatkowo określać. Mimo tych ulepszeń nasz kod nadal jest daleki od ideału. Na koniec tego rozdziału zajmiemy się modułem superagent, interfejsem programistycznym ponad interfejsem klienta HTTP, który pozwoli nam na dalszą optymalizację kodu.
MODUŁ SUPERAGENT NA POMOC Większość klientów HTTP działa na następującej zasadzie: pobiera dane odpowiedzi, parsuje ją na podstawie wartości nagłówka Content-Type, po czym przetwarza ją w odpowiedni sposób. Podczas wysyłania danych na serwer sytuacja wygląda podobnie. Używamy metody POST, a obiekt kodujemy jako JSON.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 7: PROTOKÓŁ HTTP Moduł superagenta (ang. superagent) upraszcza ten proces, rozszerzając obiekt odpowiedzi o kilka przydatnych nowości. Niektóre z nich omówimy poniżej. Przykłady z tego podrozdziału korzystają z superagenta w wersji 0.3.0. Utwórz nowy katalog i zainstaluj moduł superagent lokalnie: $ npm install
[email protected]
Aby uzyskać dane JSON dla żądania (zakładając, że serwer zwraca dane z odpowiednim nagłówkiem Content-Type, wskazującym, że odpowiedź zawiera JSON), superagent zapisze je automatycznie w buforze, sparsuje oraz udostępni jako zmienną res.body. Utwórz plik tweet.js i umieść w nim następującą zawartość: var request = require('superagent'); request.get('https://api.twitter.com/1.1/search/tweets.json') .set('Authorization', 'Bearer ' + auth) .query({ q: 'justin bieber' }) .end(function (res) { console.log(res.body); });
Po uruchomieniu pliku wyświetli się obiekt będący wynikiem odkodowania odpowiedzi JSON. Dostęp do surowych danych można uzyskać za pomocą zmiennej res.text. Zwróć uwagę na to, że superagent bardzo uprościł tworzenie ciągu zapytania: sam zajął się ustawieniem parametrów i wartości i zakodowaniem ich zgodnie z wymogami adresów URL. Nagłówek żądania ustawia się za pośrednictwem metody set (w powyższym przykładzie użyliśmy jej do ustawienia danych autoryzacji). Z kolei metoda send pozwala na łatwe wysyłanie danych w żądaniu (przydatne przy żądaniach POST). Zarówno query, send, jak i set mogą być wywoływane wielokrotnie, interfejs jest zatem progresywny; umożliwia wykonanie szeregu operacji na obiekcie i wywołanie metody end po zakończeniu. Prostota tego interfejsu nie kończy się na żądaniach GET. Metody put, post, head oraz del są udostępniane przez superagenta w podobny sposób. Poniżej pokazano, jak wysłać obiekt metodą POST w formacie JSON: var request = require('superagent'); request.post('http://example.com/') .send({ json: 'encoded' }) .end();
JSON jest domyślnym kodowaniem. Jeśli chcesz je zmienić, po prostu ustaw odpowiednią wartość nagłówka Content-Type żądania, wywołując metodę set.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
129
CZĘŚĆ II: NAJISTOTNIEJSZE INTERFEJSY PROGRAMISTYCZNE NODE
PRZEŁADOWANIE SERWERA ZA POMOCĄ NARZĘDZIA UP Z pewnością zauważyłeś, że nieustanne przeładowywanie procesu Node, aby zmiany wprowadzone w kodzie serwera za pomocą edytora tekstu zostały odzwierciedlone również w przeglądarce, jest dosyć uciążliwe. Najprostszym, ale nieco naiwnym rozwiązaniem tego problemu jest przeładowywanie procesu po każdej wykrytej modyfikacji kodu. Takie podejście może wystarczyć, kiedy tworzymy aplikację lokalnie, w środowisku produkcyjnym warto jednak zadbać o to, by żądania będące „w locie” (niezakończone żądania podczas przeładowywania kodu) nie były unicestwiane wraz z procesem. Na szczęście, stworzyłem narzędzie o nazwie up, które radzi sobie z tym problemem w bezpieczny i niezawodny sposób. Do lokalnej instalacji up wystarczy użyć menedżera pakietów: $ npm install -g up
130
Aby umożliwić działanie narzędzia, konieczna jest nieco inna organizacja kodu. Zamiast wywoływania metody listen, serwer HTTP Node, który chcesz przeładować, musi zostać wyeksportowany, ponieważ up wywoła metodę listen samodzielnie i będzie do tego potrzebować instancji serwera. Jako przykład utwórz nowy katalog i umieść w nim plik server.js z następującą zawartością: module.exports = require('http').createServer(function (req, res) { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end('Witaj świecie'); });
Przejdź teraz do tego katalogu za pomocą polecenia cd, uruchom serwer, przywołując narzędzie up z parametrami --watch oraz --port: $ up --watch --port 80 server.js
Opcja --watch użyje odpowiednich interfejsów Node do wykrycia zmian w jakimkolwiek pliku katalogu roboczego. Spróbuj wpisać w przeglądarce adres serwera, po czym edytuj plik server.js, zmieniając tekst Witaj świecie. Gdy tylko zapiszesz plik, odśwież okno przeglądarki, a wprowadzone zmiany zostaną natychmiast odzwierciedlone!
PODSUMOWANIE W tym rozdziale omówiliśmy szczegółowo wykorzystanie protokołu HTTP w Node. Zaczęliśmy od omówienia podstaw HTTP z punktu widzenia protokołu zbudowanego ponad warstwą TCP. W przykładzie Witaj świecie przyjrzałeś się dokładnie domyślnej odpowiedzi generowanej przez Node. Wiesz już, które nagłówki są domyślne i dlaczego tak jest.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 7: PROTOKÓŁ HTTP Nagłówki odgrywają bardzo istotną rolę w żądaniach HTTP. Teraz wiesz już również, jak zmienić domyślne nagłówki w odpowiedziach serwera. Masz też podstawową wiedzę na temat formatów używanych przy wymianie danych z przeglądarkami oraz narzędzi oferowanych przez Node, służących do parsowania i pracy z danymi wejściowymi. Po utworzeniu prostego serwera WWW przyjrzeliśmy się też interfejsom programistycznym klienta, które bardzo ułatwiają wymianę danych przez usługi sieciowe we współczesnym internecie. Po analizie najczęstszych przypadków użycia wysłaliśmy zapytanie do interfejsu programistycznego Twittera. Okazało się jednak, że nasz kod nie jest optymalny, ponieważ pewne jego fragmenty są powielane. Rozwiązaniem problemu okazał się nowy interfejs programistyczny bazujący na rdzeniu Node.JS. Modułem Connect, który spełnia taką samą rolę przy serwerach WWW, zajmiemy się w następnym rozdziale, jako że wymaga on szerszego omówienia. Na końcu zaprezentowałem up, narzędzie wiersza poleceń (dostępne również w formie interfejsu programistycznego JavaScript) ułatwiające testowanie tworzonych serwerów po wprowadzeniu zmian w kodzie. Zapamiętaj: aby narzędzie up mogło poprawnie działać, tworzony moduł musi eksportować instancję http.Server zwracaną przez metodę createServer.
131
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
CZĘŚĆ III
TWORZENIE APLIKACJI SIECIOWYCH
Rozdział 8. „Framework Connect” Rozdział 9. „Framework Express” Rozdział 10. „Technologia WebSocket” Rozdział 11. „Framework Socket.IO”
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
8 ROZDZIAŁ
PODRĘCZNIK NODE.JS
FRAMEWORK CONNECT
NODE.JS OFERUJE szereg prostych interfejsów programistycznych wspomagających wykonywanie najbardziej typowych zadań przy tworzeniu aplikacji sieciowych. Dotychczas omówiliśmy podstawowe interfejsy dla serwerów TCP oraz interfejs dla znajdującego się na wyższej warstwie protokołu HTTP. Większość rzeczywistych aplikacji wykonuje szereg typowych zadań, których każdorazowa implementacja za pomocą standardowych interfejsów mogłaby być nieco uciążliwa. Framework Connect to zestaw narzędzi dla serwera HTTP wspomagający nowy sposób organizacji kodu zawierającego żądania i odpowiedzi, który opiera się na szeregu metod pośredniczących (ang. middleware).
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
Aby zilustrować korzyści płynące z możliwości wielokrotnego wykorzystania kodu, jaką daje nam framework Connect, rozważmy przykładową strukturę strony: $ ls website/ index.html images/
W katalogu images mamy cztery obrazy: $ ls website/images/ 1.jpg 2.jpg 3.jpg 4.jpg
Dokument index.html ma za zadanie wyświetlić cztery obrazy. Efekt możesz zobaczyć pod adresem http://localhost lub na rysunku 8.1: Moja strona
CZĘŚĆ III: TWORZENIE APLIKACJI SIECIOWYCH
136
Rysunek 8.1. Prosta statyczna strona demonstrująca możliwości frameworka Connect Aby pokazać, w jaki sposób framework Connect może usprawnić proces tworzenia aplikacji HTTP, w tym rozdziale stworzymy prostą stronę przy użyciu natywnego modułu http, a następnie to samo zrobimy, posługując się interfejsem connect.
PROSTA STRONA INTERNETOWA PRZY UŻYCIU MODUŁU HTTP Zaczniemy, jak zwykle, od dołączenia modułów http (do obsługi serwera) oraz fs (do odczytu plików): /** * Zależności modułów */ var http = require('http') , fs = require('fs')
Następnie zainicjalizujemy serwer i obsłużymy cykl żądanie-odpowiedź: /** * Utwórz serwer. */
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 8: FRAMEWORK CONNECT var server = http.createServer(function (req, res) { // … });
Na koniec włączymy serwer, wywołując metodę listen: /** * Nasłuchuj. */ server.listen(3000);
Wróćmy teraz do funkcji zwrotnej z metody createServer. Trzeba sprawdzić, czy adres URL odpowiada jakiemuś plikowi w katalogu, a jeśli tak — odczytać i wyświetlić plik. W przyszłości możemy zechcieć dodać kolejne obrazy, kod powinien być zatem wystarczająco dynamiczny, aby uwzględnić taką ewentualność. W pierwszym kroku sprawdzamy, czy metodą żądania jest GET i czy adres URL rozpoczyna się ciągiem znaków /images, a kończy ciągiem .jpg. Jeżeli adres URL ma postać '/', serwer zwraca plik index.html (za pomocą funkcji serve, którą zajmiemy się później). W pozostałych przypadkach zwracamy kod odpowiedzi 404 Not Found: if ('GET' == req.method && '/images' == req.url.substr(0, 7) && '.jpg' == req.url.substr(-4)) { // … } else if ('GET' == req.method && '/' == req.url) { serve(__dirname + '/index.html', 'text/html'); } else { res.writeHead(404); res.end('Nie Znaleziono'); }
Użyjemy teraz metody fs.stat, aby sprawdzić, czy plik istnieje. Stała globalna __dirname odnosi się do katalogu macierzystego serwera. W pierwszym bloku if umieść następującą instrukcję: fs.stat(__dirname + req.url, function (err, stat) { });
Zauważ, że nie używamy tu synchronicznego odpowiednika fs.stat (fs.statSync). Zablokowałoby to pozostałe żądania, które nie mogłyby zostać przetworzone do czasu odnalezienia plików na dysku, co jest niepożądane, jeśli serwer ma obsługiwać dużą współbieżność. Więcej informacji na ten temat znajdziesz w rozdziale 3. W przypadku błędu proces jest zamykany, a serwer wysyła kod odpowiedzi HTTP 404, sygnalizujący problem ze znalezieniem obrazu. To samo powinno się stać, jeśli metoda stat nie zakończy się błędem, ale wskazana ścieżka nie jest plikiem. W funkcji zwrotnej przekazywanej do fs.stat umieść następujący fragment kodu: if (err || !stat.isFile()) { res.writeHead(404);
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
137
CZĘŚĆ III: TWORZENIE APLIKACJI SIECIOWYCH res.end('Nie znaleziono'); return; }
W pozostałych sytuacjach serwer zwraca obraz. Po bloku if wstaw w kodzie następujący wiersz: serve(__dirname + req.url, 'application/jpg');
Na koniec utworzymy funkcję serve, która — jak się być może domyślasz — przyjmuje jako parametry ścieżkę do zwracanego pliku oraz wartość nagłówka 'Content-Type', dzięki któremu przeglądarka rozpoznaje typ wysyłanego zasobu: function serve (path, type) { res.writeHead(200, { 'Content-Type': type }); fs.createReadStream(path).pipe(res); }
Czy przypominasz sobie punkt o strumieniach z rozdziału 6.? Odpowiedź HTTP jest strumieniem tylko do zapisu. Z pliku można z kolei utworzyć strumień tylko do odczytu. Strumień systemu plików może zostać następnie przekształcony za pomocą metody pipe w odpowiedź HTTP! Powyższy fragment kodu jest odpowiednikiem przedstawionego poniżej:
138
fs.createReadStream(path) .on('data, function (data) { res.write(data); }) .on('end', function () { res.end(); })
Jest to najbardziej efektywny i zalecany sposób zwracania statycznych plików. Łącząc wszystko w całość, otrzymujemy: /** * Zależności modułów. */ var http = require('http') , fs = require('fs') /** * Utwórz serwer. */ var server = http.createServer(function (req, res) { if ('GET' == req.method && '/images' == req.url.substr(0, 7) && '.jpg' == req.url.substr(-4)) { fs.stat(__dirname + req.url, function (err, stat) { if (err || !stat.isFile()) { res.writeHead(404); res.end('Nie znaleziono');
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 8: FRAMEWORK CONNECT return; } serve(__dirname + req.url, 'application/jpg'); }); } else if ('GET' == req.method && '/' == req.url) { serve(__dirname + '/index.html', 'text/html'); } else { res.writeHead(404); res.end('Nie znaleziono'); } function serve (path, type) { res.writeHead(200, { 'Content-Type': type }); fs.createReadStream(path).pipe(res); } }); /** * Nasłuchuj. */ server.listen(3000);
I to już koniec! Teraz wystarczy tylko uruchomić skrypt: $ node server
Po wpisaniu w przeglądarce adresu http://127.0.0.1:3000 powinieneś zobaczyć gotową stronę.
PROSTA STRONA INTERNETOWA PRZY UŻYCIU FRAMEWORKA CONNECT Ten przykład obejmuje kilka często wykonywanych czynności w procesie tworzenia stron internetowych:
zwracanie plików statycznych,
obsługę błędów oraz nieprawidłowych lub brakujących adresów URL,
obsługę różnych typów żądań.
Framework Connect, jako warstwa ponad interfejsem programistycznym http, posiada pewne mechanizmy pozwalające usprawnić implementację tych często zachodzących procesów i skoncentrować się na właściwych zadaniach aplikacji, zgodnie z zasadą DRY (od ang. Don’t Repeat Yourself — Nie powtarzaj się). Nasz poprzedni przykład można znacznie uprościć dzięki Connect. Na początek utwórz plik package.json i zdefiniuj w nim następujące zależności:
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
139
CZĘŚĆ III: TWORZENIE APLIKACJI SIECIOWYCH { "name": "moja-strona" , "version": "0.0.1" , "dependencies": { "connect": "1.8.7" } }
Następnie zainstaluj pakiet: $ npm install
I dołącz go za pomocą polecenia require: /** * Zależności modułów. */ var connect = require('connect')
Teraz możesz już utworzyć obiekt http.Server przy użyciu Connect: /** * Utwórz serwer. */
140
var server = connect.createServer();
Użyjemy metody pośredniczącej static, przekazując ją jako parametr do metody use(). Pojęcie metody pośredniczącej wyjaśnimy w następnym podrozdziale, a w kolejnych rozdziałach omówimy bardziej szczegółowo. Na razie musisz tylko wiedzieć, że metoda pośrednicząca to prosta funkcja JavaScript. W tym przypadku konfigurujemy metodę pośredniczącą connect.static, przekazując jej kilka parametrów. /** * Obsłuż statyczne pliki. */ server.use(connect.static(__dirname + '/website'));
Plik index.html i katalog images umieść w katalogu /website, aby serwer nie zwracał niepożądanych plików. Teraz możesz włączyć serwer poleceniem listen: /** * Nasłuchuj. */ server.listen(3000);
Gotowe! Connect radzi sobie dobrze również z błędami 404, możesz zatem spróbować przejść do /fikcyjnego-adresu-url.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 8: FRAMEWORK CONNECT
METODY POŚREDNICZĄCE Aby przybliżyć metody pośredniczące, wróćmy na chwilę do przykładu, w którym użyliśmy modułu http Node. Nie wnikając zbytnio w szczegóły, skoncentrujmy się na głównych zadaniach naszego kodu: if ('GET' == req.method && '/images' == req.url.substr(0, 7)) { // zwróć obraz } else if ('GET' == req.method && '/' == req.url) { // zwróć plik index } else { // wyświetl błąd 404 }
Jak widać, aplikacja może wykonać tylko jedno zadanie przy każdym zgłoszonym żądaniu. Jeżeli chcesz na przykład zapisywać żądania w pliku dziennika, poprzedzisz powyższy kod wierszem: console.error(' %s %s ', req.method, req.url);
Wyobraź sobie teraz, że większa aplikacja potrafi wykonać wiele różnych zadań, w zależności od pewnych zmiennych związanych z każdym żądaniem; na przykład:
Zapisać w pliku dziennika żądania i czas ich trwania.
Zwracać statyczne pliki.
Wykonać autoryzację.
Obsługa tych zadań w pojedynczej metodzie (funkcji zwrotnej przekazywanej do metody createServer) byłaby dosyć skomplikowanym procesem. Mówiąc najprościej, metody pośredniczące składają się z funkcji obsługujących obiekty req i res, otrzymujących także funkcję next, umożliwiającą kontrolę przepływu. Ta sama aplikacja stworzona przy użyciu metod pośredniczących wyglądałaby tak: server.use(function (req, res, next) { // zapisujemy w pliku dziennika zawsze console.error(' %s %s ', req.method, req.url); next(); }); server.use(function (req, res, next) { if ('GET' == req.method && '/images' == req.url.substr(0, 7)) { // zwróć obraz } else { // przekaż zadanie innej metodzie pośredniczącej next(); } });
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
141
CZĘŚĆ III: TWORZENIE APLIKACJI SIECIOWYCH server.use(function (req, res, next) { if ('GET' == req.method && '/' == req.url) { // zwróć plik index } else { // przekaż zadanie innej metodzie pośredniczącej next(); } }); server.use(function (req, res, next) { // ostatnia metoda pośrednicząca; jeśli dotarliśmy tu, nie wiadomo, co zrobić res.writeHead(404); res.end('Nie znaleziono'); });
Taka struktura aplikacji zwiększa przejrzystość kodu (aplikacja jest rozbita na mniejsze części), a dodatkową korzyścią jest możliwość jego wielokrotnego wykorzystania. Connect zawiera szereg metod wykonujących typowe przy tworzeniu aplikacji sieciowych zadania. Aby zapisywać żądania w pliku dziennika, wystarczy tylko użyć instrukcji: app.use(connect.logger('dev'))
Naprawdę nie trzeba nic więcej!
142
W następnym punkcie pokażemy, jak stworzyć metodę pośredniczącą, której zadaniem będzie monitorowanie czasu odpowiedzi żądania.
TWORZENIE METOD POŚREDNICZĄCYCH WIELOKROTNEGO UŻYTKU Metoda informująca o zbyt długim czasie trwania żądania może być przydatna w wielu sytuacjach. Wyobraź sobie na przykład, że masz stronę internetową, która wysyła szereg żądań do bazy danych. Przeprowadzone testy wykazały, że czas odpowiedzi na ogół nie przekracza 100 milisekund (ms), ale każde dłuższe przetwarzanie żądania chcesz odnotować w pliku dziennika. Zacznij od utworzenia modułu (pliku) o nazwie request-time.js. Celem modułu jest udostępnienie funkcji zwracającej funkcję. Jest to wzorzec charakterystyczny dla konfigurowalnych metod pośredniczących. Wywołując metodę connect.logger w poprzednim przykładzie, przekazaliśmy jej parametr, w wyniku czego zwrócona została funkcja, która przetworzyła ostatecznie żądanie. Aktualnie moduł przyjmuje tylko jedną opcję — liczbę milisekund, po której zapiszemy żądanie w pliku dziennika jako problematyczne: /** * Metoda pośrednicząca czasu żądania. * * Opcje: * - 'time' ('Number'): liczba milisekund, po której zapisujemy (100) *
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 8: FRAMEWORK CONNECT * @param {Object} options * @api public */ module.exports = function (opts) { // … };
Najpierw ustawimy domyślną wartość 100: var time = opts.time || 100;
Następnie zwrócimy funkcję, która stanie się metodą pośredniczącą: return function (req, res, next) {
W samej metodzie pośredniczącej używamy wywołania funkcji z opóźnieniem (zostanie ona wywołana po określonym czasie): var timer = setTimeout(function () { console.log( '\033[90m%s %s\033[39m \033[91mtrwa zbyt długo!\033[39m' , req.method , req.url ); }, time);
Trzeba pamiętać o zatrzymaniu odliczania w przypadku, kiedy żądanie kończy się przed upływem 100 ms. Innym często spotykanym rozwiązaniem, jeśli chodzi o metody pośredniczące, jest przesłanianie funkcji, tak aby jeżeli są wywoływane przez inne metody pośredniczące, mogły one wykonać pewne zadanie. W tym przypadku po zakończeniu odpowiedzi wywołaniem end() zatrzymujemy odliczanie: var end = res.end; res.end = function (chunk, encoding) { res.end = end; res.end(chunk, encoding); clearTimeout(timer); };
Najpierw uzyskujemy referencję do oryginalnej funkcji (var end = res.end). Potem w ciele przesłoniętej funkcji przywracamy oryginał, wywołujemy go, a następnie zatrzymujemy odliczanie. Wreszcie, aby przekazać sterowanie do następnej metody pośredniczącej, wywołujemy metodę next. Bez tego aplikacja nie mogłaby działać! next();
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
143
CZĘŚĆ III: TWORZENIE APLIKACJI SIECIOWYCH Oto pełny kod metody pośredniczącej: /** * Metoda pośrednicząca czasu żądania. * * Opcje: * - 'time' ('Number'): liczba milisekund, po której zapisujemy (100) * * @param {Object} options * @api public */ module.exports = function (opts) { var time = opts.time || 100; return function (req, res, next) { var timer = setTimeout(function () { console.log( '\033[90m%s %s\033[39m \033[91mtrwa zbyt długo!\033[39m' , req.method , req.url ); }, time); var end = res.end; res.end = function (chunk, encoding) { res.end = end; res.end(chunk, encoding); clearTimeout(timer); }; next();
144
}; };
Możemy teraz przetestować przykład, tworząc za pomocą Connect prostą aplikację testującą dwa możliwe scenariusze: wolnego i szybkiego przetwarzania żądania. Zacznij od zdefiniowania zależności:
server.js /** * Zależności modułów. */ var connect = require('connect') , time = require('./request-time')
Następnie utwórz serwer: /** * Utwórz serwer. */ var server = connect.createServer();
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 8: FRAMEWORK CONNECT Zapisz żądania w pliku dziennika: /** * Zapisz żądania w pliku dziennika. */ server.use(connect.logger('dev'));
Zaimplementuj metodę pośredniczącą: /** * Implementacja metody pośredniczącej. */ server.use(time({ time: 500 }));
Zaimplementuj szybką obsługę żądań: /** * Szybka odpowiedź. */ server.use(function (req, res, next) { if ('/a' == req.url) { res.writeHead(200, {'Content-Type': 'text/html charset=utf-8'}); res.end('Krótko!'); } else { next(); } });
oraz symulowaną wolną obsługę żądań: /** * Wolna odpowiedź. */ server.use(function (req, res, next) { if ('/b' == req.url) { setTimeout(function () { res.writeHead(200, {'Content-Type': 'text/html charset=utf-8'}); res.end('Długo!'); }, 1000); } else { next(); } });
Na koniec, tradycyjnie, włącz serwer: /** * Nasłuchuj. */ server.listen(3000);
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
145
CZĘŚĆ III: TWORZENIE APLIKACJI SIECIOWYCH Teraz możesz go już uruchomić: $ node server
W przeglądarce internetowej odwiedź adres http://localhost:3000/a (scenariusz szybkiego przetwarzania). Efekt pokazano na rysunku 8.2.
Rysunek 8.2. Scenariusz szybkiego przetwarzania żądania (/a) w przeglądarce
146
Na rysunku 8.3 pokazano natomiast metodę pośredniczącą logger w akcji.
Rysunek 8.3. Raporty metody pośredniczącej logger po przejściu do adresu /a Rysunek 8.4 pokazuje alternatywny scenariusz, w którym żądanie przetwarzane jest długo. Rysunek 8.5 przedstawia konsolę i metodę pośredniczącą w akcji! W dalszej części rozdziału omówimy wybrane metody pośredniczące wbudowane we framework Connect z racji ich przydatności do wielokrotnego użycia w różnych aplikacjach sieciowych.
METODA POŚREDNICZĄCA STATIC Jedną z najczęściej wykorzystywanych podczas tworzenia aplikacji sieciowych w Node metod pośredniczących jest metoda static.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 8: FRAMEWORK CONNECT
Rysunek 8.4. Odpowiedź serwera w „wolnym” scenariuszu /b
Rysunek 8.5. Wpisy metody logger przy żądaniach adresu /b zawierają ostrzeżenie wygenerowane przez naszą pierwszą metodę pośredniczącą
Przypisywanie Connect pozwala na podłączenie (ang. mounting) metody pośredniczącej do danego adresu URL. W przypadku metod takich jak static jest to bardzo wygodne rozwiązanie, dzięki któremu możemy odwzorować dowolny adres URL na konkretny katalog w systemie plików. Załóżmy na przykład, że chcesz obsłużyć katalog nazwany /images przy żądaniu adresu /my-images. Możesz to zrobić za pomocą przypisania: server.use('/my-images', connect.static('/path/to/images'));
Opcja maxAge Metoda static przyjmuje jako parametr opcję maxAge, która określa, jak długo dane żądanie ma być przechowywane w pamięci podręcznej aplikacji klienta. Takie rozwiązanie przydaje się szczególnie w sytuacjach, kiedy wiesz, że dany zasób nie zmieni się i nie ma potrzeby, aby przeglądarka ponawiała żądanie. Częstą praktyką w aplikacjach sieciowych jest na przykład łączenie wszystkich skryptów JavaScript po stronie serwera w jeden plik reprezentujący numer wersji. Możesz ustawić wysoką wartość maxAge, aby tego typu pliki były przechowywane w pamięci podręcznej w nieskończoność: server.use('/js', connect.static('/path/to/bundles', { maxAge: 10000000000000 });
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
147
CZĘŚĆ III: TWORZENIE APLIKACJI SIECIOWYCH
Opcja hidden Kolejną opcją metody static jest hidden. Jeżeli ma ona wartość true, Connect obsłuży również pliki, których nazwa zaczyna się kropką (.) i które są ukryte (ang. hidden) w systemach plików Unix: server.use(connect.static('/path/to/resources', { hidden: true });
METODA POŚREDNICZĄCA QUERY Czasami w aplikacji zdarza się, że przesyłamy w adresie URL dodatkowe dane, jako część łańcucha zapytania. Weźmy na przykład adres /blog-posts?page=5. Jeśli wskażesz go w przeglądarce, Node przypisze zmiennej req.url ciąg znaków: server.use(function (req) { // req.url == "/blog-posts?page=5" });
Tak naprawdę jednak najczęściej będziemy potrzebować wartości zawartej w łańcuchu zapytania.
148
Implementując metodę pośredniczącą query, uzyskujemy obiekt req.query automatycznie wypełniony tymi wartościami: server.use(connect.query); server.use(function (req, res) { // req.query.page == "5" });
Parsowanie łańcucha zapytania jest kolejnym typowym zadaniem w wielu aplikacjach, które dzięki Connect staje się dziecinnie proste. Podobnie jak w przykładzie z metodą static, gdzie nie było potrzeby dołączania za pomocą require modułu fs, metoda query pozwala nam zapomnieć o module querystring. Ta metoda pośrednicząca jest automatycznie dostępna również we frameworku Express, który jest tematem następnego rozdziału. Nie wybiegajmy jednak w przyszłość i przejdźmy teraz do kolejnej bardzo użytecznej metody: logger.
METODA POŚREDNICZĄCA LOGGER Metoda pośrednicząca logger jest przydatnym narzędziem diagnostycznym w aplikacji sieciowej. Pozwala wyświetlić w terminalu informacje o przychodzących żądaniach i wychodzących odpowiedziach. Informacje mogą być wyświetlone w jednym z czterech wbudowanych formatów:
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 8: FRAMEWORK CONNECT
default,
dev,
short,
tiny.
Jeśli chcemy na przykład skorzystać z formatu dev, metodę logger zainicjalizujemy następująco: server.use(connect.logger('dev'));
Rozważmy przykład standardowego serwera HTPP typu „Witaj świecie” wykorzystującego metodę pośredniczącą logger: var connect = require('connect'); connect.createServer( connect.logger('dev') , function (req, res) { res.writeHead(200 , { 'Content-Type': 'text/html; charset=utf-8' }); res.end('Witaj świecie'); } ).listen(3000);
Zauważ, że jako parametry przekazaliśmy do metody createServer szereg funkcji. Taki sam efekt moglibyśmy osiągnąć, inicjalizując serwer Connect i wywołując dwukrotnie metodę use. Teraz, po wprowadzeniu w przeglądarce adresu http://127.0.0.1:3000, w konsoli powinny zostać wyświetlone dwa wiersze: GET / 200 0ms GET /favicon.ico 200 2ms
Przeglądarka żąda /favicon.ico oraz /, a metoda logger wyświetla metodę żądania, kod odpowiedzi HTTP i czas wykonania. dev jest zwięzłym formatem, umożliwiającym wgląd w działanie i wydajność aplikacji podczas jej testowania. Metoda logger dopuszcza również użycie własnych łańcuchów do określenia formatu wyświetlanych danych. Załóżmy, że chcesz zapisać w pliku dziennika tylko metodę żądania oraz adres IP: server.use(connect.logger(':method :remote-addr'));
Możesz też korzystać z dynamicznych skrótów req i res. Aby zapisać długość zawartości (content-length), typ zawartości (content-type) oraz czas wykonania żądania, użyj instrukcji: server.use(connect.logger('typ: ' + res[content-type] +', długość: ' + res[content-length] +', wygenerowano w ' + response-time + ' ms.'));
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
149
CZĘŚĆ III: TWORZENIE APLIKACJI SIECIOWYCH
Uwaga: Pamiętaj, że w Node nagłówki żądań i odpowiedzi pisane są małymi literami. Stosując powyższy kod do przykładu strony żądającej czterech obrazów, można się spodziewać wyników podobnych do przedstawionych na rysunku 8.6 (przed uruchomieniem tego przykładu upewnij się, że pamięć podręczna przeglądarki jest wyczyszczona).
Rysunek 8.6. Program zapisujący w dzienniku w akcji Poniżej prezentuję pełną listę skrótów, z których można korzystać:
150
:req[header] (na przykład :req[Accept])
:res[header] (na przykład :res[Content-Length])
:http-version :response-time
:remote-addr :date :method :url
:referrer :user-agent
:status
Możesz też definiować swoje własne skróty. Powiedzmy, że chcesz odnosić się do typu zawartości żądania (Content-Type) za pomocą skrótu :type; użyjesz wtedy kodu: connect.logger.token('type', function (req, res) { return req.headers['content-type']; });
Następna metoda pośrednicząca to parser ciała żądania.
METODA POŚREDNICZĄCA BODYPARSER W jednym z przykładów wykorzystujących moduł http użyliśmy modułu qs, aby sparsować treść żądania POST.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 8: FRAMEWORK CONNECT Framework Connect może nam w tym również pomóc! Po zastosowaniu metody bodyParser w następujący sposób: server.use(connect.bodyParser());
uzyskujemy dostęp do danych POST w obiekcie req.body: server.use(function (req, res) { // req.body.myinput });
Obiekt req.body jest wypełniany danymi, nawet jeśli żądanie POST jest wysyłane z klienta w postaci JSON, ponieważ metoda bodyParser sprawdza również nagłówek Content-Type żądania.
Obsługa plików wysyłanych na serwer Metoda bodyParser przydaje się też przy korzystaniu z modułu formidable, który pozwala uzyskać dostęp do plików wysyłanych na serwer przez użytkownika. W tym przykładzie użyjemy skróconej wersji wywołania createServer, w której przekażemy jako parametry wszystkie potrzebne metody pośredniczące: var server = connect( connect.bodyParser() , connect.static('static') );
W katalogu static/ utwórz plik index.html z prostym formularzem umożliwiającym wysyłanie plików: Wyślij plik!
Następnie dodaj prostą metodę pośredniczącą, aby zobaczyć zawartość obiektu req.files.file: function (req, res, next) { if ('POST' == req.method) { console.log(req.files.file); } else { next(); }}
Możesz teraz przetestować serwer! Spróbuj wysłać plik Witaj.txt, jak pokazano na rysunku 8.7.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
151
CZĘŚĆ III: TWORZENIE APLIKACJI SIECIOWYCH
Rysunek 8.7. Przesłanie z przeglądarki przykładowego pliku tekstowego na serwer Spójrz na informacje zwrócone przez serwer. Pokazano je na rysunku 8.8.
152
Rysunek 8.8. Reprezentacja obiektu przesłanego pliku w obiekcie req.files wyświetlona w konsoli Jak widać, otrzymaliśmy obiekt opisujący przesłany plik za pomocą kilku interesujących właściwości. Informacje te odeślemy również do użytkownika: if ('POST' == req.method && req.files.file) { fs.readFile(req.files.file.path, 'utf8', function (err, data) { if (err) { res.writeHead(500); res.end('Błąd!'); return; } res.writeHead(200, { 'Content-Type': 'text/html' }); res.end([ 'Plik: ' + req.files.file.name + '' , 'Typ: ' + req.files.file.type + '' , 'Zawartość:' + data + '' ].join('')); }); } else { next(); }
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 8: FRAMEWORK CONNECT Przesłanie pliku powoduje wyświetlenie jego zawartości (zob. rysunek 8.9).
Rysunek 8.9. Zawartość pliku Witaj.txt wyświetlana przez przeglądarkę po przesłaniu pliku na serwer
Wiele plików Aby przesłać kilka plików naraz, wystarczy dodać [] na końcu atrybutu name elementu input:
Zmienna req.files.files zawiera tablicę obiektów, takich jakie widzieliśmy w przykładzie z plikiem Witaj.txt.
CIASTECZKA Connect, działając na podobnej zasadzie co metoda query, może pomóc również w procesie parsowania i udostępniania ciasteczek. Kiedy przeglądarka wysyła ciasteczka, wykorzystuje do tego nagłówek Cookie. Ma on format nieco podobny do łańcucha zapytania w adresie URL. Zobaczmy przykładowe żądanie z tym nagłówkiem: GET /secret HTTP/1.1 Host: www.mywebsite.org Cookie: secret1=value; secret2=value2 Accept: */*
Aby uzyskać dostęp do wartości secret1 i secret2 bez konieczności manualnego parsowania czy korzystania z wyrażeń regularnych, możemy posłużyć się metodą pośredniczącą cookieParser: server.use(connect.cookieParser())
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
153
CZĘŚĆ III: TWORZENIE APLIKACJI SIECIOWYCH Tak jak należało oczekiwać, dostęp do wartości uzyskujemy poprzez obiekt req.cookies: server.use(function (req, res, next) { // req.cookies.secret1 = "wartość" // req.cookies.secret2 = "wartość2" })
METODA POŚREDNICZĄCA SESSION Większość aplikacji sieciowych używa mechanizmu określanego jako „sesja użytkownika”, który oferuje identyfikację użytkownika wysyłającego żądania. Każde logowanie na stronie internetowej wiąże się z jakąś formą systemu sesji ustawiającego wartość ciasteczka w przeglądarce, które jest następnie wysyłane razem z kolejnymi żądaniami. Connect bardzo ułatwia korzystanie z sesji użytkownika. Jako przykład w tym punkcie stworzymy prosty system logowania. Dane użytkowników będziemy zapisywać w pliku users.json zgodnie z poniższym wzorem: {
154
"tobi": { "password": "fretka" , "name": "Tobiasz Holowaychuk" } }
Zacznij od dołączenia frameworka Connect i pliku z danymi użytkowników: /** * Zależności modułów */ var connect = require('connect') , users = require('./users')
Zauważ, że za pomocą polecenia require można dołączać również pliki JSON! Jeżeli chcesz eksportować tylko dane, nie musisz używać polecenia module.export; wystarczy, że wyeksportujesz je bezpośrednio, jako JSON. Następnie określ, z jakich metod pośredniczących będziesz korzystać. W tym przypadku są to metody: logger, bodyParser i session. Ponieważ sesje użytkownika polegają między innymi na wysłaniu ciasteczka do użytkownika, przed metodą pośredniczącą session musi się pojawić metoda cookieParser: var server = connect( connect.logger('dev') , connect.bodyParser() , connect.cookieParser() , connect.session({ secret: 'mój klucz' })
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 8: FRAMEWORK CONNECT Ze względów bezpieczeństwa przy inicjalizacji metody pośredniczącej session należy podać wartość opcji secret, która zabezpiecza ciasteczko. Pierwsza dodatkowa metoda pośrednicząca sprawdza, czy użytkownik jest zalogowany; jeżeli nie — przekazuje żądanie dalej: , function (req, res, next) { if ('/' == req.url && req.session.logged_in) { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end( 'Witaj z powrotem, ' + req.session.name + '. ' + 'Wyloguj' ); } else { next(); } }
Druga wyświetla formularz logowania: , function (req, res, next) { if ('/' == req.url && 'GET' == req.method) { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end([ '' , '' , 'Zaloguj się' , 'Użytkownik: ' , 'Hasło: ' , 'Wyślij' , '' , '' ].join('')); } else { next(); } }
Kolejna sprawdza, czy użytkownik istnieje, i loguje go do aplikacji: , function (req, res, next) { if ('/login' == req.url && 'POST' == req.method) { res.writeHead(200{ 'Content-Type': 'text/html; charset=utf-8' }); if (!users[req.body.user] || req.body.password != users[req.body.user].password) { res.end('Zła nazwa użytkownika lub hasło'); } else { req.session.logged_in = true; req.session.name = users[req.body.user].name; res.end('Zalogowano pomyślnie!'); } } else {
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
155
CZĘŚĆ III: TWORZENIE APLIKACJI SIECIOWYCH next(); } }
Zauważ, że modyfikujemy tu obiekt sesji req.session. Jest on automatycznie zapisywany za każdym razem, kiedy wysyłana jest odpowiedź, nie musimy się zatem o to troszczyć. W zmiennej name przechowujemy nazwę użytkownika, a o tym, że jest on zalogowany, świadczy wartość true, którą nadajemy zmiennej logged_in. Na końcu w podobny sposób obsługujemy wylogowywanie z aplikacji. , function (req, res, next) { if ('/logout' == req.url) { req.session.logged_in = false; res.writeHead(200); res.end('Wylogowano!'); } else { next(); } }
Kod skończonej aplikacji powinien wyglądać tak:
156
/** * Zależności modułów */ var connect = require('connect') , users = require('./users') /** * Utwórz serwer */ var server = connect( connect.logger('dev') , connect.bodyParser() , connect.cookieParser() , connect.session({ secret: 'mój klucz' }) , function (req, res, next) { if ('/' == req.url && req.session.logged_in) { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end( 'Witaj z powrotem, ' + req.session.name + '. ' + 'Wyloguj' ); } else { next(); } }
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 8: FRAMEWORK CONNECT , function (req, res, next) { if ('/' == req.url && 'GET' == req.method) { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end([ '' , '' , 'Zaloguj się' , 'Użytkownik: ' , 'Hasło: ' , 'Wyślij' , '' , '' ].join('')); } else { next(); } } , function (req, res, next) { if ('/login' == req.url && 'POST' == req.method) { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); if (!users[req.body.user] || req.body.password != users[req.body.user].password) { res.end('Zła nazwa użytkownika lub hasło'); } else { req.session.logged_in = true; req.session.name = users[req.body.user].name; res.end('Zalogowano pomyślnie!'); } } else { next(); } } , function (req, res, next) { if ('/logout' == req.url) { req.session.logged_in = false; res.writeHead(200); res.end('Wylogowano!'); } else { next(); } } ); /** * Nasłuchuj. */ server.listen(3000);
Przetestujmy teraz ten prosty system logowania. Na początek, tak jak pokazano na rysunkach 8.10 i 8.11, sprawdź, czy zabezpieczenia działają poprawnie.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
157
CZĘŚĆ III: TWORZENIE APLIKACJI SIECIOWYCH
Rysunek 8.10. Próba logowania z nieprawidłowymi danymi użytkownika
158 Rysunek 8.11. Próba zalogowania z nieprawidłowymi danymi skończyła się niepowodzeniem Spróbuj teraz, tak jak pokazano na rysunku 8.12, zalogować się za pomocą danych jednego z użytkowników w pliku users.json.
Rysunek 8.12. Próba logowania za pomocą danych istniejącego użytkownika Potwierdzenie pomyślnego logowania pokazano na rysunku 8.13. Aby stworzyć mechanizm sesji użytkownika nadający się do implementacji w środowisku produkcyjnym, musimy nauczyć się je przechowywać w bazie danych Redis.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 8: FRAMEWORK CONNECT
Rysunek 8.13. Potwierdzenie wyświetlane po zalogowaniu Na rysunku 8.14 widać stronę główną po pomyślnym zalogowaniu.
159 Rysunek 8.14. Efekt przejścia do poprzedniej strony po zalogowaniu
SESJE REDIS Spróbuj następującej rzeczy: będąc zalogowanym w aplikacji, uruchom ponownie serwer i odśwież okno przeglądarki. Zauważ, że sesja wygasła! Dzieje się tak, ponieważ domyślnym magazynem dla metody pośredniczącej session jest pamięć. Oznacza to, że wraz z zakończeniem procesu wygasa również przechowywana w pamięci sesja użytkownika. Ten stan rzeczy nie przeszkadza przy tworzeniu aplikacji, w środowisku produkcyjnym byłby jednak raczej niepożądany. Należy wtedy zastąpić magazyn takim, który pozwoli uruchomić ponownie aplikację Node bez utraty danych — na przykład Redis (szersze omówienie Redis znajdziesz w rozdziale 12.). Redis jest małą, szybką bazą danych wykorzystywaną przez moduł connect-redis do przechowywania danych sesji, tak aby znajdowały się one poza procesem Node. Możesz ją zainicjalizować w następujący sposób (konieczna jest instalacja Redis): var connect = require('connect') , RedisStore = require('connect-redis')(connect);
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
CZĘŚĆ III: TWORZENIE APLIKACJI SIECIOWYCH W dalszej kolejności musisz przekazać opcję store do metody pośredniczącej session: server.use(connect.session({ store: new RedisStore, secret: 'mój klucz' }))
Gotowe! Sesje są już w stanie przetrwać zakończenie procesu Node.
METODA POŚREDNICZĄCA METHODOVERRIDE Starsze przeglądarki internetowe nie potrafią tworzyć żądań (na przykład w technologii Ajax) niektórych typów, takich jak żądania PUT, DELETE czy PATCH. Popularnym sposobem na pozbycie się tej niedoskonałości przeglądarek jest wysłanie żądania GET lub POST i umieszczenie dodatkowej zmiennej _method w łańcuchu zapytania adresu URL z rzeczywistą metodą. Na przykład, aby przesłać zasób metodą PUT z Internet Explorera, żądanie powinno wyglądać następująco: POST /url?_method=PUT HTTP/1.1
Jeśli żądanie ma zostać potraktowane przez metody pośredniczące jako PUT, należy dołączyć metodę pośredniczącą methodOverride: server.use(connect.methodOverride())
160
Pamiętaj o tym, że metody pośredniczące są wykonywane sekwencyjnie, tak więc metoda methodOverride powinna być dołączona przed dołączeniem innych metod przetwarzających żądanie.
METODA POŚREDNICZĄCA BASICAUTH Przy niektórych projektach wystarczy nam elementarna warstwa uwierzytelniania (zob. rysunek 8.15) kontrolowana przez aplikację klienta. Framework Connect bardzo ułatwia dodanie tej warstwy dzięki metodzie pośredniczącej basicAuth. Jako przykład stworzymy prymitywny system autoryzacji, polegający na uwierzytelnianiu użytkowników przez administratorów z poziomu wiersza poleceń.
Rysunek 8.15. Okno logowania wyświetlane przez przeglądarkę (podstawowa procedura uwierzytelniania)
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 8: FRAMEWORK CONNECT Najpierw pobierzemy dane od użytkownika: process.stdin.resume(); process.stdin.setEncoding('ascii');
A następnie dodamy metodę pośredniczącą basicAuth: connect.basicAuth(function (user, pass, fn) { process.stdout.write('Zezwól na dostęp użytkownikowi \033[96m' + user + '\033[39m ' + 'z hasłem \033[90m' + pass + '\033[39m ? [t/n]: '); process.stdin.once('data', function (data) { if (data[0] == 't') { fn(null, { username: user }); } else fn(new Error('Unauthorized')); }); })
Zwróć uwagę na użycie metody once obiektu process.stdin. Dane z wiersza poleceń chcemy otrzymać tylko raz na żądanie1. Metoda basicAuth jest bardzo prosta w użyciu. Dostarcza parametry user (nazwa użytkownika) i pass (hasło) oraz funkcję zwrotną, która zostanie wywołana po udanej lub nieudanej autoryzacji. Jeżeli autoryzacja przebiegła pomyślnie, jako pierwszy argument przekazujemy wartość null (obiekt Error, jeśli się nie uda), a dodatkowo obiekt użytkownika, który zostanie podstawiony do zmiennej req.remoteUser. Następnie deklarujemy kolejną metodę pośredniczącą, która jest wykonywana tylko w przypadku pomyślnej autoryzacji: , function (req, res) { res.writeHead(200); res.end('Witamy w zabezpieczonym dziale, ' + req.remoteUser.username); }
Teraz możemy już podać dane użytkownika (zob. rysunek 8.16).
Rysunek 8.16. Okno logowania wypełnione przykładowymi danymi 1
„Once” oznacza po angielsku „raz” — przyp. tłum.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
161
CZĘŚĆ III: TWORZENIE APLIKACJI SIECIOWYCH (Nie)bezpieczną autoryzację z poziomu wiersza poleceń pokazano na rysunku 8.17!
Rysunek 8.17. Żądanie uwierzytelnienia zostaje wyświetlone w konsoli serwera Rysunek 8.18 przedstawia stronę po uwierzytelnieniu użytkownika z poziomu wiersza poleceń.
162
Rysunek 8.18. Po zezwoleniu użytkownikowi na dostęp w terminalu żądanie jest autoryzowane
PODSUMOWANIE W tym rozdziale poznaliśmy zalety korzystania z metod pośredniczących, jeśli chodzi o lepszą organizację kodu i możliwość jego wielokrotnego wykorzystania. Moduł Connect dysponuje infrastrukturą, która pozwala to osiągnąć przy stosunkowo małym wysiłku. Porównaliśmy pojedynczą metodę obsługi żądania z podzieloną na mniejsze logiczne części, a połączoną funkcją next, sekwencją metod pośredniczących. Przyjrzeliśmy się najważniejszym metodom pośredniczącym frameworka Connect, które rozwiązują problemy najczęściej napotykane przy budowie stron internetowych i aplikacji sieciowych. Po lekturze tego rozdziału potrafisz już pisać swoje własne metody pośredniczące i wiesz, jak dzięki systemowi modułów Node.JS korzystać z nich wielokrotnie.
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
9 ROZDZIAŁ
PODRĘCZNIK NODE.JS
FRAMEWORK EXPRESS
PODCZAS GDY FRAMEWORK CONNECT OFERUJE mechanizmy wspomagające szereg podstawowych czynności, które bez niego wykonywane byłyby za pomocą modułu http, framework Express dostarcza wygodny interfejs programistyczny do budowy całych stron i aplikacji sieciowych, bazujący w całości na Connect. Być może zauważyłeś podczas analizy przykładów z rozdziału 8., że większość zadań podczas interakcji przeglądarki internetowej i serwera koncentruje się wokół metod i adresów URL. Kombinacja dwóch ostatnich określana jest czasami jako
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
trasa (ang. route). Jest to kluczowe pojęcie dla aplikacji Express. Express bazuje na frameworku Connect, który z kolei hołduje zasadzie wielokrotnego wykorzystywania metod pośredniczących, które wykonują pewne podstawowe zadania. Otrzymujemy dzięki temu interfejs programistyczny o dużej sile wyrazu, ale zachowujący jednocześnie wygodne komponenty wielokrotnego użytku operujące ponad modułem http. Aby zobaczyć tę siłę wyrazu w praktyce, napiszemy prostą aplikację opartą w całości na frameworku Express, pozwalającą na przeszukiwanie zasobów Twittera.
CZĘŚĆ III: TWORZENIE APLIKACJI SIECIOWYCH
PROSTA APLIKACJA EXPRESS Nasza aplikacja, choć niezbyt skomplikowana, będzie z założenia dynamiczna. Kiedy użytkownik zażąda „tweetów” dla wprowadzonego kryterium wyszukiwania, trzeba będzie je mu dostarczyć w formie HTML. Tym razem, zamiast łączyć łańcuchy znaków składające się na kod HTML w naszych metodach obsługi, wykorzystamy proste szablony, które pozwolą nam oddzielić logikę kontrolera (ang. controller) od widoku (ang. view). W pierwszym kroku musimy dołączyć wszystkie potrzebne nam do tego moduły.
TWORZYMY MODUŁ Tak jak zawsze utwórz plik package.json, ale tym razem zadeklaruj dwie dodatkowe zależności: ejs, system szablonów w tym przykładzie, i moduł superagenta, aby uprościć żądania HTTPS przy przeszukiwaniu Twittera. { "name": "express-tweet" , "version": "0.0.1" , "dependencies": { "express": "3.0.2" , "ejs": "0.4.2" , "superagent": "0.3.0" }
164 }
Pod zdefiniowaniu metadanych dla projektu w kolejnym kroku stworzymy szablony, które wygenerują potrzebny kod HTML.
HTML W odróżnieniu od poprzednich aplikacji, tym razem nasz kod HTML generowany będzie przy użyciu prostego języka szablonów, dzięki czemu pozbędziemy się go z logiki aplikacji. Język ten nazywa się EJS (Embedded JavaScript) i działa na podobnej zasadzie, co osadzanie kodu PHP w HTML. Zacznij od umieszczenia pliku index.ejs w katalogu view/. Właściwie szablony mogłyby znajdować się gdziekolwiek, ale aby zachować uporządkowaną strukturę projektu, lepiej umieścić je w osobnym katalogu. Pierwszy szablon będzie zwracany dla domyślnej trasy (strony głównej). Prosi on użytkownika o wprowadzenie kryterium wyszukiwania tweetów: Aplikacja Twitter Wprowadź kryterium wyszukiwania:
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 9: FRAMEWORK EXPRESS Szukaj
Drugi szablon to wyniki wyszukiwania, search.ejs. Wyróżnia on wyszukiwaną frazę i wyświetla iteracyjnie wyniki (jeśli są) lub odpowiedni komunikat: Tweety znalezione dla - Brak wyników
Jak łatwo zauważyć, kod JavaScript umieszczany jest pomiędzy parą specjalnych znaczników EJS: . Aby wyświetlić zmienne, potrzebny jest dodatkowy znak "=" po
Kod wyjściowy wygenerowanego skryptu wygląda następująco:
out.js (function(){var global = this;function debug(){return debug};function require(p, parent){ var path = require.resolve(p) , mod = require.modules[path]; if (!mod) throw new Error('nieudany import "' + p + '" z ' + parent); if (!mod.exports) { mod.exports = {}; mod.call(mod.exports, mod, mod.exports, require.relative(path), global); } return mod.exports;}require.modules = {};require.resolve = function(path){ var orig = path , reg = path + '.js' , index = path + '/index.js'; return require.modules[reg] && reg || require.modules[index] && index || orig;};require.register = function(path, fn){ require.modules[path] = fn;};require. relative = function(parent) { return function(p){ if ('debug' == p) return debug; if ('.' != p.charAt(0)) return require(p); var path = parent.split('/') , segs = p.split('/'); path.pop(); for (var i = 0; i < segs.length; i++) { var seg = segs[i]; if ('..' == seg) path.pop(); else if ('.' != seg) path.push(seg); } return require(path.join('/'), parent); };};require.register("main.js", function(module, exports, require, global){ var log = require('./log');
301
module.exports = function () { log('Kod modułu wykonany'); } });require.register("log.js", function(module, exports, require, global){ module.exports = function (str) { return console.log(str); } });mymodule = require('main'); })();
Pierwszy fragment skryptu to implementacja funkcji require, która używana jest w dalszej jego części. Skompilowana jest do tylko jednego wiersza kodu, dzięki czemu zmniejsza się rozmiar pliku. Warto również zwrócić uwagę na fragment, w którym funkcja require.register udostępnia parametr global. Dzięki temu nie trzeba sprawdzać, czy obiekt window istnieje, aby udostępnić daną zmienną globalną. W trakcie tworzenia skryptów możemy bazować na obiekcie Node global, który w przeglądarce będzie wskazywał obiekt window. Ostatnia część kodu udostępnia zmienną globalną (w tym przypadku mymodule) i wyjaśnia, dlaczego musimy poinformować narzędzie browserbuild o tym, który moduł jest głównym. mymodule = require('main');
301 Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
CZĘŚĆ V: TESTOWANIE Kolejną interesującą możliwością w procesie kompilacji za pomocą narzędzia browserbuild jest if node, który pozwala użyć komentarzy JavaScript, aby poinformować kompilator, że pewien fragment kodu powinien zostać pominięty podczas kompilacji modułu dla przeglądarki.
nodeonly.js // if node process.exit(1); // end console.log('przeglądarka i node');
W powyższym przykładzie blok umieszczony między komentarzami zostanie pominięty. Po uruchomieniu polecenia $ browserbuild --main nodeonly nodeonly.js
wiersz process.exit nie znajdzie się w kodzie wyjściowym:
302
// … require.register("nodeonly.js", function(module, exports, require, global){ console.log('przeglądarka i node'); });nodeonly = require('nodeonly'); })();
Aby dowiedzieć się więcej o dostępnych opcjach, wydaj polecenie browserbuild --help lub odwołaj się do strony projektu znajdującej się pod adresem http://github.com/learnboost/ browserbuild.
PODSUMOWANIE Jak wielokrotnie podkreślaliśmy w tej książce, tworzenie kodu JavaScript po stronie serwera to — dzięki Node.JS — czysta przyjemność. Najistotniejszą innowacją jest system modułów, który nie ma swojego odpowiednika w środowisku przeglądarek internetowych. Ten rozdział rozpoczęliśmy od poznania sposobów tworzenia kodu, działającego zarówno po stronie serwera, jak i po stronie przeglądarki, w czasie wykonania programu. Dzięki kontroli typów za pomocą operatora typeof możemy wykryć dostępne właściwości systemu modułów i zapewnić alternatywny mechanizm udostępniania dla przeglądarki (na przykład zmienne globalne). Manualne „opakowywanie” kodu w samowywołujące się funkcje i wykonywanie porównań typeof dla każdego pliku w bibliotece potrafią skutecznie zabić prostotę systemu importowania plików w Node. Problem ten likwiduje narzędzie browserbuild. Można dzięki niemu tworzyć moduły dla Node.JS, które mogą zostać skompilowane i uruchomione w przeglądarce przy minimalnym narzucie.
302 Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 15: WSPÓŁDZIELONY KOD Największą zaletą tego rozwiązania jest fakt, że biblioteka udostępniana jest pod postacią zmiennej globalnej w środowisku przeglądarki, zupełnie jak jQuery czy IO, co oznacza, że nie narzucamy użytkownikowi końcowemu konkretnego interfejsu systemu modułów, aby mógł wykorzystać nasz kod.
303
303 Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
TESTOWANIE
DO TEJ PORY ZA KAŻDYM razem, kiedy kończyliśmy nowy program, weryfikowaliśmy poprawność jego działania, uruchamiając go i obserwując, czy zachowuje się zgodnie z oczekiwaniami. Ta metoda testowania często nie wystarcza do zapewnienia poprawnej pracy programów oraz do uniknięcia potencjalnych usterek w przyszłości. Automatyczne testowanie jest procesem polegającym na uruchomieniu szeregu programów w celu weryfikacji, czy zamierzone działanie funkcji pokrywa się z oczekiwanym. Zaczniemy od stworzenia
Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
jednego małego programu na każdy test, przy czym pomoże nam moduł assert. W dalszej części rozdziału zoptymalizujemy proces tworzenia testów sprawdzających (ang. assertions), korzystając z projektu expect.js. Następnie pokażemy, jak usystematyzować proces testowania przy użyciu frameworka Mocha. Na końcu powiemy, co można zrobić z istniejącymi testami kodu źródłowego przeznaczonego dla serwera i dla przeglądarek, aby uruchomić je w przeglądarce.
ROZDZIAŁ 16
PODRĘCZNIK NODE.JS
CZĘŚĆ V: TESTOWANIE
PROSTE TESTY Punktem wyjścia do testowania jest zawsze identyfikacja przedmiotu. Mówiąc inaczej, musimy zdecydować, dla których skryptów i funkcji będziemy tworzyć testy.
PRZEDMIOT TESTÓW Przedmiotem naszych testów w tym rozdziale będzie aplikacja wyszukująca tweety z rozdziału 9. Napiszemy program, który będzie sprawdzał, czy po wprowadzeniu kryterium wyszukiwania zwracana jest lista tweetów. Program zrobi to, szukając pewnych charakterystycznych fragmentów kodu HTML — w tym przypadku listy tweetów złożonej z przynajmniej jednego elementu . Potwierdzenie obecności kryterium wyszukiwania i obecności ciągu znaków w odpowiedzi HTTP powinno wystarczyć do stwierdzenia poprawnego działania aplikacji. Na początek, wróć do kodu tego przykładu i uruchom aplikację. Następnie wpisz w przeglądarce adres http://localhost:3000.
STRATEGIA TESTÓW 306
Najprostszą formą testowania jest utworzenie nowego programu Node, którego kod zakończenia wynosi 0, jeżeli test zostanie zaliczony, i 1w przeciwnym przypadku. Kiedy w Node zgłaszany jest nieobsłużony wyjątek, program kończy się automatycznie, zwracając kod błędu. Takie zachowanie bardzo nam odpowiada. Dodatkową informacją jest ślad stosu wykonań, pozwalający dokładnie zidentyfikować miejsce wystąpienia błędu. Tworząc zatem testy, będziemy się starali potwierdzić spełnienie pewnych warunków albo zgłosić wyjątek. Node oferuje do tego celu specjalny moduł podstawowy assert, który sprawdza spełnienie danego warunku albo zgłasza wyjątek AssertionError. Jako przykład, utworzymy prosty test, który kończy się sukcesem, jeżeli stempel czasowy jest liczbą parzystą, a porażką, jeśli nieparzystą: /** * Zależności modułu. */ var assert = require('assert'); /** * Sprawdź warunek */ var now = Date.now(); console.log(now); assert.ok(now % 2 == 0);
306 Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 16: TESTOWANIE Użycie assert.ok gwarantuje, że przekazana wartość będzie interpretowana jak true (choć niekoniecznie musi mieć wartość true). Jeżeli liczba dzieli się przez 2 bez reszty, jest parzysta. Uruchom teraz kilka razy program i spójrz na stemple czasowe: simple-testing node assert-example.js 1325520251830 simple-testing node assert-example.js 1325520252742 simple-testing node assert-example.js 1325520253637 node.js:134 throw e; // process.nextTick error, or 'error' event on first tick ^ AssertionError: true == false at Object. (assert-example.js:14:8) at Module._compile (module.js:411:26) at Object..js (module.js:417:10) at Module.load (module.js:343:31) at Function._load (module.js:302:12) at Array. (module.js:430:10) at EventEmitter._tickCallback (node.js:126:26)
W dwóch pierwszych przypadkach są one liczbami parzystymi, program kończy się zatem bez żadnych nieprawidłowości. Przy trzecim wykonaniu stempel czasowy jest liczbą nieparzystą; otrzymujemy wtedy ślad stosu wywołań.
PROGRAM TESTOWY Wykorzystamy teraz moduł superagenta do wykonania żądania GET z kryterium wyszukiwania bieber i analizy odpowiedzi: /** * Zależności modułu. */ var request = require('superagent') , assert = require('assert') /** * Testuje /search?q= */ request.get('http://localhost:3000') .data({ q: 'bieber' }) .exec(function (res) { // potwierdź poprawność kodu odpowiedzi assert.ok(200 == res.status);
307 Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
307
CZĘŚĆ V: TESTOWANIE // potwierdź obecność kryterium wyszukiwania assert.ok(~res.text.toLowerCase().indexOf('bieber')); // potwierdź obecność elementów listy assert.ok(~res.text.indexOf('')); });
Zauważ, że jeżeli żądanie wygeneruje błąd, zostanie zgłoszony nieobsłużony wyjątek, który zakończy program. Przed uruchomieniem testów pamiętaj o instalacji modułu superagenta: npm install
[email protected]
EXPECT.JS W poprzednich przykładach używaliśmy metody assert.ok i prostego wyrażenia JavaScript. Niektóre wyrażenia w testach mogą być na pierwszy rzut oka niezrozumiałe. Na przykład najprostszym sposobem na sprawdzenie, czy jeden ciąg znaków zawiera drugi, jest połączenie metody indexOf i operatora ~, jak w przykładzie powyżej.
308
Moduł expect.js oferuje prostą funkcję expect, która zamienia assert.ok(~res.text.indexOf(''));
na expect(res.text).to.contain(''));
Drugie wyrażenie zdecydowanie bardziej przypomina język naturalny; dzięki temu dużo łatwiej zrozumieć i tworzyć testy. Projekt expect.js dostępny jest w menedżerze pakietów jako expect.js, a jego pełną dokumentację można znaleźć pod adresem https://github.com/learnboost/expect.js. W dalszej części tego podrozdziału omówimy niektóre podstawowe interfejsy modułu expect.js.
PRZEGLĄD INTERFEJSÓW PROGRAMISTYCZNYCH Funkcję expect uzyskujemy poprzez import modułu expect.js: var expect = require('expect.js')
Expect.js współpracuje z każdym modułem, który działa z modułem assert. Podobnie jak funkcje udostępniane przez moduł assert, zgłasza wyjątek AssertionError, jeśli oczekiwane warunki nie są spełnione.
308 Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
ROZDZIAŁ 16: TESTOWANIE Oto niektóre z najbardziej przydatnych metod udostępnianych przez moduł expect.js w wersji 0.1.2:
ok: Sprawdza, czy wartość jest interpretowana jako true. expect(1).to.be.ok(); expect(true).to.be.ok(); expect({}).to.be.ok(); expect(0).to.not.be.ok();
be/equal: Potwierdza równość ===. expect(1).to.be(1); expect(NaN).not.to.equal(NaN); expect(1).not.to.be(true); expect('1').to.not.be(1);
eql: Potwierdza słabą równość obiektów. expect({ a: 'b' }).to.eql({ a: 'b' }); expect(1).to.eql('1');
a/an: Potwierdza typ wartości (używając wewnętrznego operatora typeof); obsługuje przy tym tablice i operator instanceof. // typeof z obsługą tablic expect(5).to.be.a('number'); expect([]).to.be.an('array'); // działa expect([]).to.be.an('object'); // również działa, ponieważ używa typeof // konstruktory expect(5).to.be.a(Number); expect([]).to.be.an(Array); expect(tobi).to.be.a(Ferret); expect(person).to.be.a(Mammal);
309
match: Potwierdza dopasowanie ciągu znaków do wyrażenia regularnego. expect(program.version).to.match(/[0-9]+\.[0-9]+\.[0-9]+/);
contain: Sprawdza, czy dana konstrukcja zawiera inną dla ciągów znaków i tablic. expect([1, 2]).to.contain(1); expect('Witaj świecie').to.contain('świecie');
length: Potwierdza długość tablicy. expect([]).to.have.length(0); expect([1,2,3]).to.have.length(3);
empty: Sprawdza, czy tablica jest pusta. expect([]).to.be.empty(); expect([1,2,3]).to.not.be.empty();
property: Sprawdza obecność własnej właściwości (i opcjonalnie jej wartości). expect(window).to.have.property('expect');expect(window).to.have. property('expect', expect) expect({a: 'b'}).to.have.property('a');
key/keys: Potwierdza obecność klucza; modyfikator only pozwala stwierdzić, czy jest to jedyny klucz (klucze) w tablicy. js expect({ a: 'b' }).to.have.key('a'); expect({ a: 'b', c: 'd' }).to.only.have.keys('a', 'c');
309 Ebookpoint.pl kopia dla: Michal Nowak
[email protected]
CZĘŚĆ V: TESTOWANIE expect({ a: 'b', c: 'd' }).to.only.have.keys(['a', 'c']); expect({ a: 'b', c: 'd' }).to.not.only.have.key('a');
throwException: Sprawdza, czy wywołanie funkcji powoduje zgłoszenie wyjątku. expect(fn).to.throwException(); expect(fn2).to.not.throwException();
within: Sprawdza, czy liczba mieści się w przedziale. expect(1).to.be.within(0, Infinity);
greaterThan/above: Potwierdza relację >. expect(3).to.be.above(0); expect(5).to.be.greaterThan(3);
lessThan/below: Potwierdza relację