235 80 6MB
Polish Pages 328 [327] Year 2014
Tytuł oryginału: Smashing Node.js: JavaScript Everywhere Tłumaczenie: Krzysztof Wołowski ISBN: 978-83-246-6677-5 This edition first published 2012 © 2012 Guillermo Rauch All Rights Reserved. Authorised translation from the English language edition published by John Wiley & Sons Limited. Responsibility for the accuracy of the translation rests solely with Helion S.A. and is not the responsibility of John Wiley & Sons Limited. No part of this book may be reproduced in any form without the written permission of the original copyright holder, John Wiley & Sons Limited. Translation copyright © 2014 by Helion S.A. Wiley and the John Wiley & Sons, Ltd. logo are trademarks or registered trademarks of John Wiley & Sons, Inc. and/or its affiliates in the United States and/or other countries, and may not be used without written permission. All other trademarks are the property of their respective owners. John Wiley & Sons, Ltd. is not associated with any product or vendor mentioned in the book. Wszelkie prawa zastrzeżone. Nieautoryzowane rozpowszechnianie całości lub fragmentu niniejszej publikacji w jakiejkolwiek postaci jest zabronione. Wykonywanie kopii metodą kserograficzną, fotograficzną, a także kopiowanie książki na nośniku filmowym, magnetycznym lub innym powoduje naruszenie praw autorskich niniejszej publikacji. Wszystkie znaki występujące w tekście są zastrzeżonymi znakami firmowymi bądź towarowymi ich właścicieli. Wydawnictwo HELION dołożyło wszelkich starań, by zawarte w tej książce informacje były kompletne i rzetelne. Nie bierze jednak żadnej odpowiedzialności ani za ich wykorzystanie, ani za związane z tym ewentualne naruszenie praw patentowych lub autorskich. Wydawnictwo HELION nie ponosi również żadnej odpowiedzialności za ewentualne szkody wynikłe z wykorzystania informacji zawartych w książce. Wydawnictwo HELION ul. Kościuszki 1c, 44-100 GLIWICE tel. 32 231 22 19, 32 230 98 63 e-mail: [email protected] WWW: http://helion.pl (księgarnia internetowa, katalog książek) Pliki z przykładami omawianymi w książce można znaleźć pod adresem: ftp://ftp.helion.pl/przyklady/podnod.zip Drogi Czytelniku! Jeżeli chcesz ocenić tę książkę, zajrzyj pod adres http://helion.pl/user/opinie/podnod_ebook Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję.
Poleć książkę na Facebook.com Kup w wersji papierowej Oceń książkę
Ebookpoint.pl kopia dla: Michal Nowak [email protected]
Księgarnia internetowa Lubię to! » Nasza społeczność
PODZIĘKOWANIA WYDAWCY ORYGINAŁU Wybrane osoby, które miały udział w przygotowaniu i publikacji tej książki: Redakcja i produkcja VP Consumer and Technology Publishing Director: Michelle Leete Associate Director–Book Content Management: Martin Tribe Associate Publisher: Chris Webb Assistant Editor: Ellie Scott Development Editor: Brian Herrmann Copy Editor: Chuck Hutchinson Technical Editor: Goddy Zhao Editorial Manager: Jodi Jensen Senior Project Editor: Sara Shlaer Editorial Assistant: Leslie Saxman Marketing Associate Marketing Director: Louise Breinholt Senior Marketing Executive: Kate Parrett Skład i korekta Compositor: Andrea Hornberger Proofreader: Linda Seifert Indexer: Potomac Indexing, LLC
O autorze Guillermo Rauch to współzałożyciel i dyrektor ds. technicznych startupu LearnBoost z branży edukacyjnej w San Francisco. Jest twórcą kilku uznanych projektów Node.JS, udzielał się także jako prelegent na JSConf i różnych warsztatach poświęconych Node.JS.
Ebookpoint.pl kopia dla: Michal Nowak [email protected]
Ebookpoint.pl kopia dla: Michal Nowak [email protected]
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
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);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);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);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 + '
');Jak masz na imię?
' , '' , 'Wyślij
' , '' ].join('')); } else if ('/url' == req.url && 'POST' == req.method) { var body = '';Twoje imię to ' + qs.parse(body).name + '
'); }); } else { res.writeHead(404); res.end('Nie znaleziono'); } }).listen(3000);Użytkownik:
' , 'Hasło:
' , 'Wyślij' , '' , '' ].join('')); } else { next(); } }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);Wprowadź kryterium wyszukiwania:
Brak wyników