Chat cu Node.js

Chat cu Node.js

V-ați distrat cu ce am învățat în postul anteriordespre web-dev cu  Node.js? Lux, hai să continuăm.

Să începem cu primirea de date de la browser. Vom face asta cu un simplu XML­HttpRe­quest. Să modificăm pagina SSE.html să conțină ur­mă­toarele elemente:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
</head>
<body><input type='text' id='message'/>
<button value='Send' id='send'>Send</button>
<script>
document.getElementById('send').addEventListener('click', sendmessage);
document.getElementById('message').addEventListener('keydown', function(event) {
    if (event.keyCode == 13) {
        sendmessage();
    }
});
function sendmessage() {
    var req = new XMLHttpRequest();
    req.open('POST', '/xhr');
    req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    req.send(document.getElementById('message').value);
    req.onreadystatechange = function (e) {
        if (req.readyState == 4) {
            if (req.status == 200)
                ;//console.log(req.responseText);
            else
                console.log("Error loading pagen");
        }
    };
}; </script>
</body>
</html>

Ce avem aici? Un tag input și un button. Codul Javascript conține o funcție și două event listeners care cheamă acea funcție: una când facem click pe butonul Submit, iar cealaltă când apăsăm Enter în căsuța de „chat”. Funcția nu face altceva decât să trimită prin POST conținutul inputului la /xhr. Cum arată acum fișierul chat.js?

var http = require('http');
var fs = require('fs');
http.createServer(function(req, res) {
        if (req.method == "POST" && req.url == '/xhr') {
            var data = '';
            req.addListener('data', function(chunk) {
                data += chunk;
            });
            req.addListener('end', function() {
                res.writeHead(200, {'Content-Type': 'text/html'});
                res.write(data);
                res.end();
                console.log(data);
            });
        } else {
            res.writeHead(200, {'Content-Type': 'text/html'});
            res.write(fs.readFileSync(__dirname + '/sse.html'));
            res.end();
        }
}).listen(8000);

Dacă cererea este prin POST și la adresa /xhr, atunci ne pregătim să primim conținutul prin POST. Mai întâi in­ițial­izăm variabila data cu un șir de caractere gol. Apoi atașăm funcții la două evenimente specifice re­ques­turilor HTTP: data și end. Eventul data se trimite atunci când se primește o bucată nouă de date, iar cel de end se primește la ... sfârșitul trimiterii. La fiecare „data” adăugăm ce s-a primit la acel event la ce am primit mai înainte, iar când s-a terminat transmisia, atunci prelucrăm. În acest caz trimitem înapoi ce am primit și scriem la consolă conținutul mesajului primit. Dacă este orice alt tip de cerere, atunci afișăm pagina sse.html.

Okay, acum e timpul să combinăm ce am învățat data trecută (trimiterea de mesaje de la server la browser) cu ce am învățat azi (pre­lu­crarea datelor primite de server).

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
</head>
<body>
    <ul>

    </ul>
    <input type='text' id='message' />
    <button value='Send' id='send'>Send</button>
    <script>
    var source = new EventSource('/events');
    source.addEventListener('message', function(e) {
        var ul = document.getElementsByTagName('ul')[0];
        var li = document.createElement('li');
        var data = e.data.split('n')
        li.innerHTML = '<strong>'+data[0]+'</strong>:'+data[1];
        ul.appendChild(li);
        console.log(e);
    }, false);

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

    source.addEventListener('error', function(e) {
      if (e.eventPhase == EventSource.CLOSED) {
        console.log('close');
      }
    }, false);
    document.getElementById('send').addEventListener('click',sendmessage);
    document.body.addEventListener('keydown',function(event) {
        if (event.keyCode == 13) {
            sendmessage();
        }
    });
    function sendmessage() {
        var req = new XMLHttpRequest();
        req.open('POST', '/xhr');
        req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
        req.send(document.getElementById('message').value);
        req.onreadystatechange = function (e) {
            if (req.readyState == 4) {
                if (req.status == 200)
                    ;//console.log(req.responseText);
                else
                    console.log("Error loading pagen");
            }
        };
    };
    </script>
</body>
</html>

Asta e aproape perfect combinația celor două fișiere de dinainte, cu excepția faptului că avem o listă în care o să inserăm mesajele de chat, iar când primim un mesaj de la server, îl despărțim la 'n'. Datele de la server vor veni astfel încât pe un rând va fi numele celui care a trimis mesajul, iar pe al doilea rând va fi mesajul propriu-zis.

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

//Array to contain active resources
var resources = new Array();

http.createServer(function(req, res) {
    if (req.headers.accept && req.headers.accept == 'text/event-stream') {
        if (req.url == '/events') {
            res.writeHead(200, {
                'Content-Type': 'text/event-stream',
                'Cache-Control': 'no-cache',
                'Connection': 'keep-alive'
            });
            //Create a random ID for each guest in the chatroom
            res.chatID = 'guest'+Math.floor(Math.random()*1000);
            resources.push(res);
            //Remove from active chat resources if connection is closed
            req.addListener('close',function (event) {
                console.log('A connection was closed');
                var index = resources.indexOf(res);
                if (index != -1) {
                    resources.splice(index,1);
                }
            });
        } else{
            res.writeHead(404);
            res.end();
        }
    } else {
        if(req.method == "POST" && req.url =='/xhr') {
            var data = '';
            req.addListener('data',function(chunk) {
                data+=chunk;
            });
            req.addListener('end',function() {
                res.writeHead(200, {'Content-Type': 'text/html'});
                res.write(data);
                res.end();
                //send the data to all the active chat resources
                for (var i=0; i<resources.length; i++) {
                    constructSSE(resources[i],2,[resources[i].chatID,data]);
                }
            });

        }
        else {
            res.writeHead(200, {'Content-Type': 'text/html'});
            res.write(fs.readFileSync(__dirname + '/sse-node.html'));
            res.end();
        }
    }
}).listen(8000);

function constructSSE(res, id, data) {
    res.write('id: ' + id + 'n');
    //Adds the capability to send multiple lines of data if they are in an array
    if (data.constructor != Array)
        data = [data];
    for (var i = 0; i<data.length; i++)
        res.write("data: " + data[i] + 'n');
    res.write('n');
}

Ce facem aici? Păi în primul rând, facem ceva ce nu putem face în PHP: stocăm într-un array fiecare response ca să știm la cine trebuie să trimitem informații înapoi. Când cineva se conecteză la event source, îl adăugăm la acest array, iar dacă se întrerupe conexiunea, îl eliminăm. Deasemenea, fiecărui response îi asociem un ID, ca să putem identifica fiecare utilizator care chatuiește. Când primim un mesaj prin XHR, îl trimitem mai departe la fiecare abonat din arrayul nostru.

Sper că ați înțeles și că v-a plăcut tutorialul. Se mai pot finisa multe la acest chat-page, dar aici e mai mult proof-of-concept. Din păcate, la hostingul meu nu pot să ofer un „serviciu” de chating pentru că nu oferă node.js :(

Fișierele le puteți descărca aici.