Introducere în Node.js

The logo of the Node.js Project from the
offic...

Image via Wikipedia

Vineri a apărut, în sfârșit, varianta oficială pentru Windows la node.js (până acuma era doar variantă neoficială, care avea or­ga­ni­zarea pe foldere genială din Unix, aka „fiecare program să facă un lucru, dar acela bine”, aka o infinitate de fișiere și ex­e­cutabile, pe când acum e doar un simplu exe), așa că am zis să văd și eu care îi acest „big deal” despre node.js.

Pentru început, pro­gra­marea web cu aceasta e complet diferită față de cum merg lucrurile în combinația Apache (sau nginx, etc) + PHP (și bănuiesc că și de Python, Ruby, etc), și am descoperit asta chinuindu-mă câteva ore bune până să îmi dau seamă cu ce se mânăncă. Serverul node trebuie in­ițial­izat cu un fișier care conține toate in­strucți­u­nile, față de Apache, pe care îl pornești fără nimica. Node.js nu pre­lu­crează deloc re­ques­turile, toate trebuie prelucrate cu mânuța, față de Apache + PHP unde Apache caută automat toate fișierele care sunt cerute, iar PHP pre­lu­crează datele GET și POST și ți le oferă într-un array fain frumos. Apoi în PHP fiecare request e separat și nu știe nimica de alte requesturi, are propriul lui thread, nu poate in­ter­acționa cu altele, pe când în node.js totul rulează în același thread și nu lucrezi la nivel de request, ci trebuie să ai grijă și de cum prelucrezi fiecare request individual. Dacă încă nu v-ați plictisit sau speriat încă, citiți mai departe pentru câteva lecții simple de node.js, urmând ca într-un post următor să facem o pagină de chat, folosind Server Sent Events :D

Să începem cu ce se începe în orice limbaj de proramare: un exemplu Hello World.

// Librăriile din node.js se numesc module și încărcăm acuma modulul pentru HTTP
var http = require('http');

// Creăm un server HTTP nou care răspunde la toate requesturile cu un header 200 și textul Hello World
var server = http.createServer(function (request, response) {
  response.writeHead(200, {"Content-Type": "text/plain"});
  response.end("Hello World!n");
});

// Ascultăm pe portul 8000, adresa de IP default este 127.0.0.0
server.listen(8000);

// Scriem la consolă ce facem
console.log("Serverul rulează la http://127.0.0.1:8000/");

Să salvam asta într-un fișier numit hello.js și să executăm în linia de comandă node hello.js, iar apoi să mergem la 127.0.0.0:8000. Ar trebui să apară Hello World!

Acuma să începem să facem o legătură mai permanentă între server și browser. Pentru aceasta vom folosi Server Sent Events, care sunt un fel de long-polling, doar că toată chestia cu re­conectare și pre­lu­crarea mesajelor este făcută de browser automat (dacă este un browser modern). Pentru asta deja ne vor trebui două fișiere: unul care să conțină pagina HTML propriu-zisă și una cu codul Javascript pentru server.

Pagina HTML, salvată sub numele de „SSE.html”, nu e prea complicată:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
</head>
<body>
    <script>
    var source = new EventSource('/events');
    source.addEventListener('message', function(e) {
        console.log(e.data);
        document.body.innerHTML += e.data + '<br>';
    }, false);

    source.addEventListener('open', function(e) {
      console.log('open');
    }, false);

    source.addEventListener('error', function(e) {
      if (e.eventPhase == EventSource.CLOSED) {
        console.log('close');
      }
    }, false);
    </script>
</body>
</html>

Sunt doar chestiile obișnuite, mai puțin în script, unde vedem un obiect nou: EventSource. Acesta acceptă ca parametru pentru con­struc­tor URL-ul care să fie sursa eveni­mentelor. Apoi atașăm listeners la 3 evenimente diferite pe care le produce acest obiect: cel de deschidere, cel de primire a unui nou mesaj și cel de închidere a conexiunii. Toate le scriem și în consolă, iar mesajele primite le și atașăm în body (știu, nu e foarte elegant cum scriu, dar pentru un tutorial va merge, imaginați-vă voi că acolo este ceva markup mai elegant).

var http = require('http');
var fs = require('fs');

http.createServer(function(req, res) {
    if (req.headers.accept && req.headers.accept == 'text/event-stream') {
        if (req.url == '/events') {
            sendSSE(res);
        } else{
            res.writeHead(404);
            res.end();
        }
    } else {
        res.writeHead(200, {'Content-Type': 'text/html'});
        res.write(fs.readFileSync(__dirname + '/sse.html'));
        res.end();
    }
}).listen(8000);

function sendSSE(res) {
    res.writeHead(200, {
                'Content-Type': 'text/event-stream',
                'Cache-Control': 'no-cache',
                'Connection': 'keep-alive'
    });
    var id = (new Date()).toLocaleTimeString();
    setInterval(function() {
        constructSSE(res, id, (new Date()).toLocaleTimeString());
    }, 5000);
    constructSSE(res, id, (new Date()).toLocaleTimeString());
}

function constructSSE(res, id, data) {
    res.write('id: ' + id + 'n');
    res.write("data: " + data + 'nn');
}

Aici începe un pic distracția și pe partea serverului. Creăm un server ca mai înainte, dar acum verificăm headerul re­ques­tu­lui și dacă acesta este 'text/event-stream' și URL-ul re­ques­tu­lui este /events atunci începem să trimitem date periodic cu funcția sendSSE. Dacă requestul are headerul de event-stream, dar la alt URL, atunci băgăm eroare 404, iar dacă este un request obișnuit, atunci citim de pe disk fișierul HTML și îl trimitem pe țeavă.

Ce face funcția sendSSE(res)? Acceptă care parametru un HTTP response, deci chestia aia pe unde vom răspunde browseru­lui. Începe prin a scrie headerele corecte, iar apoi setează id-ul mesajelor, care în cazul nostru este constant. La sfârșit iar pornește trimisul de date, o dată la 5 secunde, care în acest caz constau din timpul serverului. Ce trebuie să trimitem ca să fie valid streamul nostru de date? Dacă citim cu atenție speci­fi­cația W3C despre Server Sent Events, vedem că fiecare mesaj trebuie separat printr-o linie liberă de cel de după el ('nn' la sfârșitul funcției con­structSSE) și are cel puțin două componente: un id și un mesaj. Id-ul trebuie să fie un număr întreg, poate fi cam orice, poate fi folosit pentru a ști ce să cerem de la server în caz că se întrerupe conexiunea. Mesajul sau este prefixat de „data:” sau de un alt string, pe care apoi browserul îl va face disponibil ca un alt event. Id-ul și mesajul în sine trebuie să fie pe rânduri separate.

Dacă acum pornim serverul node.js cu fișierul proaspăt creat și mergem în browser la adresa 127.0.0.0:8000, o să ne apară la fiecare 5 secunde timpul serverului (for some reason it is off on mine :"> )

Woooo. Trimite date fără se trebuiască să facem conexiuni noi de fiecare dată. Din câte știu eu, asta nu e posibil cu PHP (conexiunea se închide după trimiterea datelor). În postul următor vom trimite date înapoi de la browser și vom face o pagină de chat de bază.

Partea 2