0% found this document useful (0 votes)
20 views

Programista 19 NodeJS

Programmer Magazine NodeJS

Uploaded by

Vundek
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
20 views

Programista 19 NodeJS

Programmer Magazine NodeJS

Uploaded by

Vundek
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 7

PROGRAMOWANIE APLIKACJI WEBOWYCH

Sebastian Rosik

Wprowadzenie do Node.js
Node.js ostatnimi czasy zyskuje coraz bardziej na popularności, częściowo dzięki
powszechności JS oraz możliwościom, jakie daje. Postaram się w prosty sposób
wyjaśnić, czym jest oraz jak działa Node.js czytelnikom, którzy jeszcze nie mieli z
nim styczności. Przedstawię, w jaki sposób pisać skrypty, instalować oraz tworzyć
własne moduły, a na końcu zaprezentuję prostą aplikację chat klient – serwer.

N ZASTOSOWANIA SIECIOWE
ode.js jest platformą bazującą na silniku V8 stworzonym przez fir-
mę Google na potrzeby swojej przeglądarki Google Chrome. V8 jest
obecnie jednym z najszybszych silników JavaScript dostępnych na W przypadku zastosowań serwerowych, w relacji klient – serwer zdecydo-
rynku. Sam Node.js ujrzał światło dzienne w 2009 r. i jak na tak młodą platfor- waną większość czasu zajmuje komunikacja pomiędzy wszystkimi punktami
mę niezwykle szybko zdobywa popularność. sieci, z której korzysta nasza aplikacja (np. połączenia z bazą danych, dostęp
Node.js jest oczywiście oparty o JavaScript, ale to wcale nie oznacza, że do dysku, logger etc.). Czas operacji czysto obliczeniowych jest jednak o wiele
jest to po prostu przeglądarka internetowa, do dyspozycji mamy silnik JS, nie mniejszy. Oczywiście nie jest to twarda reguła, a jedynie najczęściej występu-
uświadczymy wsparcia dla drzewa DOM czy styli CSS, jak to ma miejsce w jący przypadek w aplikacjach serwerowych.
przeglądarkach. Mimo że język skryptowy jest ten sam, to jednak główna za- Node.js jest sterowany zdarzeniami i nie blokuje operacji wejścia/wyjścia,
sada działania jest diametralnie inna, w szczególności jeśli chodzi o możliwo- dlatego też tak dobrze sprawdza się w rozwiązaniach serwerowych.
ści obu platform, jak i ich restrykcje. W przeglądarce internetowej na przykład
mamy ograniczenia związane z Cross-Domain Policy, brakiem modułów czy
dostępu do sprzętu. W Node.js nic nas pod tym względem nie ogranicza. Pod-
sumowując, do dyspozycji mamy sam język JavaScript na silniku V8 oraz wbu-
dowane moduły do obsługi sieci, systemu plików, procesów itp. oraz masę
modułów. Nic nas nie ogranicza pod względem możliwości.

SSJS – SERVER SIDE JAVASCRIPT


Sama koncepcja stojąca za Node.js nie jest niczym nowym i już w prze-
szłości powstało wiele podobnych rozwiązań, wszystkie mieszczące się w jed-
nej kategorii o nazwie: Server Side JavaScript. Dopiero Node.js stał się na tyle Rysunek 1. Proporcja czasu poświęconego na realizację operacji wejścia/wyjścia
(I/O) do operacji obliczeniowych (CPU)
popularny, by przejść do “mainstreamu”. Poniżej została przedstawiona lista
tylko kilku z wielu rozwiązań SSJS:
»» SilkJS (V8)
»» TeaJS (V8) NON-BLOCKING I/O
»» Aptana Jaxer (SpiderMonkey)
»» Apache Sling (Rhino) W wielu miejscach w sieci, jak i na stronie projektu przeczytamy, że Node.js
»» GromJS (SpiderMonkey) posiada nieblokujące operacje wejścia/wyjścia. Mimo iż informacja ta jest z
grubsza zgodna z prawdą, to jednak nie do końca oddaje prawdziwą naturę

PLUSY I MINUSY JAVASCRIPT ORAZ funkcjonowania tej platformy. Node.js jedynie daje nam możliwość pisania
NODE.JS skryptów w jednym wątku, który nie będzie blokowany przez operację wyj-
ścia/wejścia. Oznacza to, że możemy jednak stworzyć skrypt, który będzie się
W zależności od tego, kogo się zapyta i od jakiej strony się spojrzy, język Java- blokował. Możemy to zrobić umyślnie, poprzez wykorzystanie synchronicz-
Script jest jednocześnie olbrzymim plusem, jak i minusem. Wiele osób, przy- nych funkcji (o których więcej w dalszej części artykułu), bądź nieumyślnie,
zwyczajonych do języków o twardym typowaniu, odnosi się z niechęcią do JS poprzez błąd w kodzie lub zbyt dużą ilość obliczeń (np. parsowanie bardzo
ze względu na jego dynamiczność. Pozwala to co prawda dość szybko pisać dużego pliku).
kod, nie przejmując się typowaniem, ale odbija się to niestety na prędkości Oczywiście na mikroskopijnym poziomie już każde wywołanie jakiejkol-
działania oraz możliwości wystąpienia błędów w kodzie. wiek funkcji blokuje wątek na drobny ułamek czasu, co jest jednak jak najbar-
dziej oczekiwanym działaniem, jednak wiele wywołań takich funkcji, np. w
Plusy Minusy pętli, spowoduje całkowite zablokowanie wątku.
To samo tyczy się wywołania pojedynczej funkcji, która musi dokonać
PP Bardzo popularny język, wiele PP Ze względu na dynamikę języka, skomplikowanych obliczeń. By tego uniknąć, zawsze gdy istnieje taka możli-
osób pracujących ze środowi- łatwo napisać niechlujny kod.
skami przeglądarkowymi poczu- PP Dla osób, które do tej pory pisały wość, powinniśmy korzystać z rozwiązań asynchronicznych.
je się jak w domu. jedynie w np. PHP, może być to Funkcje w wersjach synchronicznych mogą się nam przydać w przypad-
PP Idealny do zastosowań siecio- trudna przeprawa. ku, gdy piszemy narzędzia czy aplikacje, które nie będą rozwiązaniami serwe-
wych, gdzie wymagane jest sta- PP Jako że Node.js istnieje zaledwie 4 rowymi, czyli wszędzie tam, gdzie dostęp do zasobów odbywać się będzie
łe połączenie z serwerem, bądź lata, nie ma on zbyt wielu napraw-
szybkość odpowiedzi. dę dojrzałych i komercyjnie stoso- lokalnie, a czas dostępu będzie minimalny. Funkcje synchroniczne w takim
PP Olbrzymia ilość modułów. wanych na dużą skalę bibliotek. wypadku sprawdzą się lepiej pod kątem czytelności kodu, jak i jego prostoty
(mniej zagnieżdżonych funkcji).

26 / 12 . 2013 . (19) /
WPROWADZENIE DO NODE.JS

Listing 1. Przykład, w jaki sposób można zablokować wątek, czyli Listing 4. Skrypt czytający plik tekstowy napisany w JavaScript
jak strzelić sobie w kolano (wersja synchroniczna)

// Przykład skryptu, który całkowicie zablokuje var fs = require('fs');


// wątek. var name = 'test.txt';
while(1){}; var text = fs.readFileSync( name, 'utf8');
console.log('Ten komunikat, nigdy się nie pojawi'); console.log('Zawartość pliku:');
console.log( text );
// Mimo że samo odczytanie pliku jest
// asynchroniczne i wywołanie funkcji readFile
// nie zablokuje wątku na dłużej, to jednak
// parsowanie zawartości pliku poprzez JSON.parse WĄTKOWOŚĆ
// zablokuje wątek na kilkadziesiąt milisekund.
fs.readFile("bardzo_duży_plik.json", 'utf8', Poprzez uruchomienie skryptu, w którym tworzymy nasz serwer HTTP, Node.
function(err, data){
var object = JSON.parse( data );
js tworzy tylko jeden wątek. Zatem na jedną instancję Node.js przypada tylko
}); jeden wątek, ale za to ten jeden wątek potrafi obsłużyć kilkaset lub kilka ty-
// W tym przykładzie oba wywołania funkcji
sięcy połączeń w zależności od tego, co robi nasz serwer i na jakiej maszynie
// blokują wątek. go odpalamy.
fs.readFileSync("bardzo_duży_plik.json", 'utf8'); »» Jedna instancja – jeden wątek.
var object = JSON.parse( data );
»» Jeden wątek – wiele zapytań.
»» Każde zapytanie jest obsługiwane przez ten sam wątek.

ASYNCHRONICZNOŚĆ »» Dane z jednego zapytania mogą być dostępne bezpośrednio dla drugie-
go zapytania.
JavaScript ukazuje swoją moc w asynchroniczności, dzięki której nasz skrypt
nie jest blokowany, gdy musi wykonać jakieś zadanie związane np. z dostę- PRZYKŁAD PROSTEGO SERWERA
pem do danych. HTTP
Poniżej dwa listingi plików, jeden przedstawiający skrypt napisany w PHP,
drugi w JS, oba skrypty wykonują dokładnie to samo zadanie, ale jednocze- W poprzednim przykładzie (Listing 4) skrypt odczytał plik, wypisał jego war-
śnie w diametralnie odmienny sposób. tość, a następnie zakończył działanie. Na Listingu 5 został przedstawiony naj-
Skrypt PHP jest wykonywany synchronicznie, to znaczy, że każde następ- prostszy możliwy serwer HTTP, jaki można stworzyć w Node.js. W tym kon-
ne zadanie czeka, aż pierwsze zakończy działanie. W przypadku skryptu JS kretnym przykładzie w pierwszej kolejności sięgamy po moduł serwera HTTP.
do odczytania pliku została użyta funkcja asynchroniczna, przez co zawartość Serwer HTTP tworzymy poprzez funkcję createServer, która jako argu-
pliku zostanie zwrócona w argumencie funkcji callback po pewnym czasie, ment przyjmuje funkcję, która zostanie wywołana za każdym razem, gdy jakiś
bez wstrzymywania wątku. Efekt tego będzie taki, że, mimo iż funkcja wy- klient wyśle zapytanie do serwera. Gdy skończymy inicjalizowanie naszego
świetlająca napis “Zawartość pliku:” została umieszczona na samym końcu, serwera, możemy wywołać metodę listen, co spowoduje rozpoczęcie na-
zostanie jednak wywołana przed wywołaniem funkcji, która wyświetli zawar- słuchiwania na podanym porcie i adresie, jeśli go podaliśmy.
tość pliku.
Listing 5. Prosty serwer HTTP
Listing 2. Skrypt czytający plik tekstowy napisany w PHP
var http = require('http');
// req – zawiera dane dotyczące zapytania
<?php
// res – zawiera właściwości i funkcje,
$name = "test.txt";
// dzięki którym jesteśmy w stanie
$h = fopen( $name, 'r');
// odpowiedzieć na zapytanie
$text = fread( $h, filesize( $name ) );
var visits = 0;
echo "Zawartość pliku:";
http.createServer(function( req, res ){
echo $text;
if ( req.url == '/' ) {
?>
visits++;
res.end('Jesteś gościem numer: '+visits+'\n');
Listing 3. Skrypt czytający plik tekstowy napisany w JavaScript }
(wersja asynchroniczna) }).listen(8080);

var fs = require('fs');
var name = 'test.txt';
fs.readFile( name, 'utf8', function( err, text ){
console.log( text );
});
console.log('Zawartość pliku:');

W Node.js wiele funkcji asynchronicznych posiada także swoje synchro-


niczne odpowiedniki. W przypadku użycia funkcji synchronicznych, cały wą-
tek zostanie wstrzymany aż do ukończenia działania funkcji. Wersje synchro-
niczne funkcji od razu zwracają wynik działania, nie przyjmują też funkcji jako
ostatniego argumentu, czyli tzw. funkcji Callback. Rysunek 2. Prosty serwer HTTP w akcji
W przypadku aplikacji serwerowych jest to działanie niepożądane z tego
względu, że następny klient łączący się z serwerem będzie musiał cierpli-
wie czekać, aż skrypt zakończy operację dla poprzedniego klienta. Może to Instalacja i konfiguracja
w szybkim czasie doprowadzić do sytuacji, w której większość klientów np.
otrzyma błąd o przekroczeniu czasu oczekiwania na odpowiedź. Node.js możemy pobrać ze strony http://nodejs.org w formie instalatora,
Powyższy Listing 3 wyglądałby następująco w przypadku użycia funkcji w paczki bądź kodu źródłowego. Sam proces kompilacji ogranicza się naj-
wersji synchronicznej: częściej do trzech poleceń: configure, make oraz make install (zakładając, że

/ www.programistamag.pl / 27
PROGRAMOWANIE APLIKACJI WEBOWYCH

mamy zainstalowany kompilator i system *nix). Jeżeli chcemy samemu skom- Po jej wywołaniu NPM zapyta nas o kilka podstawowych informacji (nazwa
pilować Node.js pod systemem Windows, będziemy musieli się zaopatrzyć w modułu, wersja, autor etc.). Komenda ta utworzy plik package.json, który bę-
Microsoft Visual Studio oraz Python 2.6. Link do strony z informacją na temat dzie zawierał wszystkie informacje, które wcześniej podaliśmy. Wszelkie zależ-
instalacji ze źródeł znajduje się na końcu artykułu. ności w postaci innych modułów NPM będą miały swój wpis w tym pliku, co
umożliwi bardzo prostą instalację naszego modułu w nowej lokacji poprzez
Uruchamianie skryptów wydanie komendy:

Skrypty JavaScript w Node.js uruchamiamy poprzez wydanie komendy: npm install

node plik.js Po jej wydaniu npm przeczyta wartość pola “dependencies” w pliku pac-
kage.json, po czym zainstaluje wszelkie zależności w katalogu node_modules,
Możemy także wydać powyższe polecenie z pominięciem rozszerzenia który zostanie utworzony w głównym katalogu naszego modułu.
".js" – jest ono całkowicie opcjonalne. Jeżeli nie podamy lokacji pliku jako Poniżej lista kilku z najczęściej używanych komend NPM:
argumentu, Node.js zostanie uruchomiony w trybie interaktywnym – REPL »» npm search zapytanie – szuka w repozytorium modułu pasującego do
(read-eval-print-loop). podanego zapytania
Jest to interaktywne środowisko programowania znane z wielu innych »» npm install – instaluje wszystkie moduły zdefiniowane w package.json
języków, pozwala na wpisywanie komend, wykonywanie wprowadzonego »» npm install nazwa-modulu – instaluje podany moduł w katalogu
kodu i wyświetlanie wyniku działania. node_modules
»» npm install nazwa-modulu – -save – instaluje podany moduł w katalogu
node_modules, a następnie zapisuje jego referencje w pliku package.json
»» npm list – wypisuje wszystkie zainstalowane moduły

node_module oraz require()

Moduły w Node.js możemy ładować na kilka sposobów, pierwszy z nich to


podanie ścieżki do modułu, gdzie obowiązują wszelkie reguły ścieżek rela-
tywnych, jak i absolutnych.

Listing 6. Ścieżki ładowanych modułów

// Zakładając, że lokacja skryptu to:


// /home/user/my_script
// to my_module zostanie załączony z
// /home/user/my_script/libs/my_module.js
var my_module = require('./libs/my_module');
var my_module = require('./libs/my_module.js');
var my_module = require(
Rysunek 3. REPL Node.js '/home/user/my_script/libs/my_module.js'
);

MODUŁY Drugi sposób ładowania modułów dotyczy modułów zainstalowanych


poprzez NPM. Wszelkie moduły tego typu są instalowane w katalogu node_
Node.js posiada swój oficjalny menadżer pakietów – NPM (Node Packager modules. Każdy z modułów zależnych w katalogu node_modules może posia-
Manager). Umożliwia on tworzenie, instalacje oraz zarządzanie modułami dać swoje własne zależności, a tym samym każda z dalszych zależności swój
CommonJS w prosty sposób. własny katalog node_modules.
Inicjacja nowego modułu odbywa się poprzez komendę: Skrypty, ładujące zależności poprzez nazwę modułu (Listing 7), będą szu-
kać modułu w katalogu node_modules w lokacji uruchomionego skryptu. Je-
npm init żeli moduł nie zostanie odnaleziony w tej lokacji, Node.js sprawdzi, czy kata-
log wyżej istnieje katalog node_modules, jeśli tak, spróbuje odszukać moduł
właśnie w tym katalogu. Cały proces zostanie powtórzony tyle razy, aż moduł
zostanie odnaleziony na wyższym poziomie, bądź nie zostanie odnaleziony
wcale (poprzez brak nadrzędnego katalogu).

Listing 7. Ładowanie modułu poprzez podanie jedynie nazwy modułu

var smth = require('smth');

PISZEMY PROSTY CHAT


Nasza prosta aplikacja będzie się składać z części frontendowej i backendo-
wej. Zaprezentuję też, w jaki sposób można współdzielić jeden z plików JS
pomiędzy przeglądarką a Node.js.
Aplikacja będzie prostym czatem pomiędzy wieloma użytkownikami w
jednym pokoju. Podczas pisania czatu nie uwzględnimy zagadnień związa-
Rysunek 4. Komenda npm init w akcji nych z autoryzacją użytkowników czy bezpieczeństwem, gdyż celem tej apli-
kacji jest ukazanie jedynie podstaw pisania aplikacji w Node.js.

28 / 12 . 2013 . (19) /
WPROWADZENIE DO NODE.JS

Współdzielenie skryptów Listing 10. Plik index.html

<!DOCTYPE html>
By korzystać z tego samego skryptu po stronie Node.js i przeglądarki, pierw- <html>
szą rzeczą, z jaką trzeba się zmierzyć, jest sposób rozróżnienia obu środowisk. <head>
<meta charset="utf8"/>
W Node.js nie mamy globalnego obiektu window, natomiast po stronie prze- <title>Chat</title>
glądarki nie dysponujemy systemem modułów ani nie mamy dostępu do </head>
<body>
obiektu procesu. Rozróżnienie obu środowisk zatem ograniczy się do spraw-
<form action="/room.html" method="GET">
dzenia, czy na danym środowisku istnieją odpowiednie obiekty. <input type="text"
Naszym współdzielonym skryptem będzie plik consts.js, który będzie za- name="nick"
placeholder="Twój nick"/>
wierał stałe, używane w komunikacji pomiędzy serwerem a klientem. <button type="submit">
Pierwszym etapem tworzenia modułu jest opakowanie naszego kodu w Dołącz
</button>
samowywołującą się funkcję, która w środowisku przeglądarkowym ograni- </form>
czy zasięg zmiennych tylko do tej funkcji, dzięki czemu nie będzie nam groziła </body>
</html>
potencjalna kolizja ze zmiennymi innych skryptów. Nie jest to oczywiście coś,
co musimy zrobić, ale zdecydowanie daje nam pewność, że nie wpadniemy
Listing 11. Plik room.html
w potencjalną pułapkę.
Opakowywanie zmiennych w samowywołującą się funkcję nie jest ko- <!DOCTYPE html>
niecznie w przypadku modułów Node.js, gdyż tam zmienne lokalne nie mają <html>
<head>
możliwości przebicia się do innych modułów. My natomiast i tak to zrobimy <meta charset="utf8"/>
ze względu na fakt współdzielenia tego pliku na dwóch środowiskach. <title>Chat</title>
<script src="consts.js"></script>
<script src="client.js"></script>
Listing 8. Samowywołująca się funkcja <link rel="stylesheet"
type="text/css"
(function(){ href="main.css"/>
var consts = { /**/ } </head>
})(); <body onload="app.init()">
<ul id="messages"></ul>
<footer>
Drugim etapem jest detekcja środowiska, w jakim skrypt został urucho- <input type="text" diabled />
</footer>
miony dla Node.js.
</body>
Wybór poniższych globalnych zmiennych, które biorą udział w detekcji, </html>
jest arbitralny, zatem nadadzą się jakiekolwiek globalne zmienne, jakie wystę-
pują w pierwszym środowisku, ale już nie w drugim. Listing 12. Plik main.css

Listing 9. Detekcja środowiska html, body {


margin : 0;
padding: 0;
(function(){ font-family: sans-serif;
var consts = { /**/ } }
if ( typeof module !== 'undefined' && #messages {
typeof process !== 'undefined' list-style : none;
) { position : absolute;
module.exports = consts; overflow : auto;
} else { bottom : 34px;
window.consts = consts; left : 0;
} right : 0;
})(); top : 0;
margin : 0;
Struktura plików: padding : 0;
}
»» index.html – formularz pozwalający na wpisanie nicka i dołączenie do pokoju.
»» room.html – właściwy pokój, wraz z listą wiadomości oraz polem teksto- #messages li {
margin : 0;
wym do wpisywania wiadomości. padding : 0 10px;
»» consts.js – definiuje stałe, rozróżniające typy wiadomości wysyłanych do }
i z socketów. #messages li span {
»» client.js – część aplikacji przeznaczona na frontend. font-weight : bold;
margin-right: 5px;
»» server.js – część aplikacji przeznaczona na backend. }
»» package.json – zawiera informacje na temat naszej aplikacji, takie jak na-
#messages li div {
zwa, autor, licencja, skrypt startowy i zależności. color : #999;
display : inline;
}
W pierwszej kolejności powinniśmy wywołać komendę npm init, a następ-
footer {
nie odpowiedzieć na pytania zadane nam przez konfigurator. Następnie mo-
position : absolute;
żemy już zainstalować niezależne moduły, z których będziemy korzystać: left : 0;
bottom : 0;
npm install uuid websockets – -save right : 0;
height : 34px;
background: #ccc;
}
Moduł uuid w łatwy sposób pozwoli nam wygenerować unikalne id dla
każdego z klientów łączących się z serwerem. Moduł websockets posłuży nam input {
border : 1px solid #919191;
do nawiązania połączenia pomiędzy serwerem a przeglądarką. padding : 3px;
}

/ www.programistamag.pl / 29
PROGRAMOWANIE APLIKACJI WEBOWYCH

footer input { // Inicjuje aplikację klienta po załadowaniu


margin : 0; // drzewa DOM. Wyszukuje elementy DOM pola
position : absolute; // tekstowego i listy wiadomości, po czym
left : 5px; // przypisuje zdarzenie na pole tekstowe w celu
bottom : 5px; // obsługi klawisza enter (moment zakończenia
right : 5px; // wprowadzania wiadomości).
} app.init = function() {
inputEl = document.querySelector(INP_SLCT);
msgListEl = document.querySelector(LST_SLCT);
Listing 13. Plik client.js
inputEl.addEventListener('keyup', function(e){
// Kilka użytecznych stałych switch ( e.keyCode ) {
var ENTER = 13;
case ENTER:
var ESC = 27;
e.preventDefault();
var INP_SLCT = 'footer input';
app.sendMessage(this.value);
var LST_SLCT = 'ul#messages';
this.value = '';
// Element DOM pola tekstowego do wprowadzania break;
// wiadomości oraz element listy wiadomości
case ESC:
var inputEl, msgListEl;
e.preventDefault();
// Główny obiekt naszej aplikacji this.value = '';
var app = { }
me: location.search.match(/^\?nick=(.*)/i)[1], });
connecting : false,
// Ustanawiamy połączenie gniazda
socket : null
// z serwerem WebSocket
}
this.connect();
// Funkcja do wypisywania wiadomości w liście }
app.writeMessage = function ( message ) {
// Funkcja, którą wykorzystamy do połączenia
var type = message.type || consts.USR_MSG,
// (bądź ponownego połączenia po zerwaniu
text = message.body.text || '',
// połączenia) się z serwerem
from = message.body.from || '';
app.connect = function(){
var li = document.createElement('li'), var host = location.host;
span = document.createElement('span'), this.socket = new WebSocket('ws://' + host );
div = document.createElement('div'); this.connecting = true;

li.appendChild( span ); // WebSocket obsługuje cztery typy zdarzeń:


li.appendChild( div ); //
// * open – nawiązanie połączenia
var from_el = document.createTextNode( from ), // * message – przychodząca wiadomość
text_el = document.createTextNode( text ); // z serwera
// * close – zamknięcie gniazda
// W przypadku wiadomości systemowych, nie // * error – wystąpienie błędu (np. gdy nie
// otrzymamy zdefiniowanego autora wiadomości. // można się połączyć z serwerem)
if ( from ) {
span.appendChild( from_el ); this.socket.addEventListener('open',
} function(){
app.connecting = false;
div.appendChild( text_el ); // Po nawiązaniu połączenia
// wysyłamy nick na serwer,
msgListEl.appendChild( li );
// by poinformować inne osoby obecne
// Przemieszczamy scrollbar na sam dół listy // w pokoju o fakcie połączenia się
// wiadomości, poprzez przypisanie do // nowego użytkownika.
// właściwości scrollTop nowej całkowitej app.socket.send( JSON.stringify({
// wysokości listy wiadomości (scrollHeight type : consts.JOIN,
// nie jest równy wysokości elementu, gdyż body : app.me
// bierze pod uwagę część elementu, która }));
// jest niewidoczna, gdyż znajduje się pod
// Domyślnie pole tekstowe jest
// zawinięciem).
// wyłączone, by użytkownik nie
msgListEl.scrollTop = msgListEl.scrollHeight;
// próbował wysłać wiadomości
}
// na serwer przed ustanowieniem
// Funkcja pomocniczna wysyła wiadomość na // połączenia.
// serwer, następnie dodaje wiadomość do listy inputEl.removeAttribute('disabled');
// wiadomości. });
app.sendMessage = function( text ){
this.socket.addEventListener('message',
// Przyjmijmy, iż każdą wiadomość, jaką wyślemy
function(e){
// na serwer, będziemy trzymać w obiekcie z
var message = JSON.parse(e.data);
// dwoma właściwościami.
switch ( message.type ) {
// – type – typ wiadomości, jaka ma zostać
case consts.USR_MSG:
// wysłana
case consts.SYS_MSG:
// – body – treść wiadomości będący ciągiem
app.writeMessage( message );
// znaków, bądź innym obiektem.
break;
var message = {
}
type : consts.USR_MSG,
});
body : {
text : text, this.socket.addEventListener('close',
from : this.me function(){
} console.log('CLOSE', arguments)
}; app.reconnect();
});
// Przed wysłaniem wiadomości na serwer,
// musimy skonwertować ją do postaci ciągu this.socket.addEventListener('error',
// znaków w formacie JSON. Oczywiście po function(){
// socketach możemy wysłać dowolny ciąg console.log('ERROR', arguments)
// znaków, jednak zdecydowaliśmy się na format app.connecting = false;
// JSON, by nie wymyślać koła na nowo. });
this.socket.send( JSON.stringify( message )); }
this.writeMessage( message ); // Ponownie nawiązuje połączenie z serwerem
}

30 / 12 . 2013 . (19) /
WPROWADZENIE DO NODE.JS

// w sytuacji, gdy zostanie ono zerwane. name += url.parse( req.url ).pathname;


app.reconnect = function(){ }
if ( !app.connecting ) {
// Czyścimy gniazdo. // Przed odczytaniem pliku sprawdzamy, czy on
app.socket.close(); // istnieje, jeśli tak, to wysyłamy w odpowiedzi
app.socket.removeEventListener('open'); // jego zawartość wraz z odpowiednim
app.socket.removeEventListener('message'); // Content-Type. Jeżeli jednak nie znajdziemy
app.socket.removeEventListener('close'); // pliku na dysku, musimy odpowiedzieć
app.socket.removeEventListener('error'); // błędem 404.
setTimeout(function(){ fs.exists( name, function( exists ){
app.connect(); if ( exists ) {
}, 1000); res.writeHead(200, {
} 'Content-Type' : getContentType( name )
} });
fs.readFile( name, function( err, data ){
if ( err ) throw( err );
Listing 14. Plik server.js res.end( data );
});
// Moduł serwera HTTP
} else {
// (wbudowany w node)
res.writeHead(404);
var http = require('http');
res.end();
// Moduł parsowania ścieżek do plików/katalogów }
// (wbudowany w node) });
var path = require('path'); });

// Moduł parsowania adresów URL // Wysyła wiadomość do wszystkich klientów, oprócz


// (wbudowany w node) // jednego, którego chcemy zignorować.
var url = require('url'); // Jeżeli serwer przekazuje wiadomość od jednego
// klienta do pozostałych klientów, to ważnym jest,
// Moduł systemu plików // aby wiadomości tej nie wysyłać niepotrzebnie
// (wbudowany w node) // z powrotem do nadawcy, dlatego też nadawca
var fs = require('fs'); // wiadomości jest ignorowany.
function broadcast( data, ignore_id ) {
// Moduł serwera WebSockets if ( typeof data !== 'string' ) {
// (moduł zainstalowany poprzez NPM) data = JSON.stringify( data );
var ws = require('websockets'); }
// Moduł do generowania unikalnych id for ( var id in sockets ) {
// (moduł zainstalowany poprzez NPM) if ( id != ignore_id ) {
var uuid = require('uuid'); sockets[ id ].send( data );
}
// Nasz lokalny moduł, który współdzielimy z
}
// klientem frontendowym
}
var consts = require('./consts');
// W przypadku serwera WebSocket, chcemy by
// Możemy użyć dowolnego portu i hosta,
// nasłuchiwał na tym samym porcie i hoście co
// oczywiście zakładając, że są one wolne na
// serwer HTTP, dlatego też nie będziemy
// naszej maszynie.
// bezpośrednio nasłuchiwać poprzez wywołanie
var PORT = 8080,
// metody listen(), lecz przekażemy do serwera
HOST = 'localhost';
// WebSocket referencję do serwera http.
var httpServer, wsServer; wsServer = ws.createServer({
server : httpServer
// Lista klientów, połączonych z naszym serwerem. });
var sockets = {};
// Jako pierwszy argument podajemy numer portu, pod
// Prosta funkcja pomocnicza dla określenia // jakim ma zostać uruchomiony serwer.
// Content-Type danego pliku // Dopiero na drugim miejscu znajduje się host.
function getContentType( name ) { // Jest to trochę nieintuicyjne, biorąc po uwagę
switch( path.extname( name ) ) { // fakt, iż w postaci URL dane te są podawane w
case '.html': case '.htm': // odwrotnej kolejności.
return 'text/html'; wsServer.on('connect', function( socket ){
case '.css': // Zapamiętujemy referencję do danego klienta.
return 'text/css'; var id = uuid.v1();
sockets[ id ] = socket;
case '.js':
return 'text/javascript'; console.log('New socket: %s', id);
case '.png': // Przypisujemy funkcję na zdarzenie otrzymania
return 'image/png'; // wiadomości z gniazda. W zależności od typu
// wiadomości, jaka przyjdzie, wykonujemy
default:
// odpowiednią akcję w odpowiedzi.
return 'text/plain';
socket.on('message', function( data ) {
}
message = JSON.parse( data );
}
switch ( message.type ) {
// Funkcja createServer w module HTTP tworzy
case consts.JOIN:
// nam nowy serwer, będziemy go wykorzystywać
var nick = message.body;
// głównie do wysyłania plików, żądanych
// przez klienta console.log('Join: %s', nick);
httpServer = http.createServer(function(req, res){
// Początkową ścieżkę dla każdego pliku, broadcast({
// ustawiamy na lokację naszej aplikacji. type : consts.SYS_MSG,
var name = __dirname; body : {
text : nick + ' dołączył do pokoju'
// Sprawdzamy czy jesteśmy na stronie }
// głównej (/), jeżeli tak, to powinniśmy }, id );
// w odpowiedzi wysłać plik index.html. break;
if ( /^\/$/.test( req.url ) ) {
name += '/index.html'; case consts.USR_MSG:
broadcast( data, id );
} else { break;
// moduł url – parsuje ciąg znaków URL }

/ www.programistamag.pl / 31
PROGRAMOWANIE APLIKACJI WEBOWYCH

});

socket.on('close', function(){
console.log('Socket closed: %s', id);
delete sockets[id];
});

}).listen( PORT, HOST, function(){


console.log('Server running on http://%s:%d',
HOST, PORT );
});

Listing 15. Plik consts.js

(function(){

var consts = {
USR_MSG : 1,

SYS_MSG : 2,

JOIN : 3
}

if ( typeof module !== 'undefined' &&


typeof process !== 'undefined'
) {
module.exports = consts;
} else {
window.consts = consts;
}
})(); Rysunek 5. Google Chrome z włączonym panelem DevTools. W zakładce Head-
ers widoczny jest handshake WebSockets

Powyższa aplikacja Node.js nie jest oczywiście w pełni funkcjonalną aplika-


cją. Jako demonstracja nie obsługuje wielu funkcjonalności, które pozostawiam
czytelnikowi do napisania w ramach ćwiczeń. Lista ta obejmuje między innymi:
»» Sprawdzanie, czy dany użytkownik wybrał zajęty już nick
»» Walidacja danych wprowadzonych przez użytkownika, czyli nicka i wiado-
mości (ograniczenie długości, zabezpieczenie przed XSS itp.)
»» Buforowanie wiadomości, które użytkownik chce wysłać, gdy nie ma na-
wiązanego połączenia (np. w sytuacji, gdy zerwało połączenie i klient pró-
buje na nowo się połączyć).

W sieci
PP Gotowe instalatory oraz paczki do pobrania:
http://nodejs.org/download/
PP Instalacja ze źródeł:
https://github.com/joyent/node/wiki/Installation
PP SSJS:
http://en.wikipedia.org/wiki/Comparison_of_server-side_JavaScript_
solutions#Server-side_JavaScript_use
PP Pełne API Node.js: Rysunek 6. Zakładka Frames prezentuje dane, jakie zostały wysłane i odebrane
http://nodejs.org/api/ poprzez WebSocket po ustanowieniu połączenia

Sebastian Rosik [email protected]

Frontend developer, obecnie pracuje we wrocławskim oddziale RST sp. z o.o. sp. k, gdzie
rozwija aplikacje przeglądarkowe. Dawniej brał udział w projektowaniu gier mobilnych JAVA
od strony wizualnej oraz gameplay’u. Dziś pisze głównie aplikacje HTML5 na przeglądarki
oraz zgłębia tajniki node.js. Hobbystycznie zajmuje się tworzeniem grafiki 2D.

32 / 12 . 2013 . (19) /

You might also like