Node.js – podstawy

Node.js – podstawy

Po paru słowach teorii czas na praktykę. Dzisiaj zajmiemy się prostym serwerem wykorzystującym moduł HTTP, dzięki któremu obsłużymy wszystkie zapytania. Zanim to jednak zrobimy przebrniemy przez instalację, nabierzemy trochę obycia z konsolą oraz powiemy sobie czym w Node.js są moduły i do czego służy plik package.json.

Instalacja
Uruchomienie środowiska nie powinna nam przysporzyć dużych problemów. Po prostu ściągamy plik w najświeższej wersji z oficjalnej strony Node’a i przechodzmiy kolejne kroki instalatora. W zależności od platformy możemy mieć także alternatywne metody instalacji, użytkownicy systemów linuxowych powinni sprawdzić czy niezbędne paczki nie znajdują się w ich repozytoriach.
Żeby upewnić się czy wszystko przebiegło poprawnie uruchamiamy konsole i wpisujemy:

$ node -v

czego wynikiem w moim przypadku będzie:

v0.10.0

Instalacja zakończona sukcesem, czas na zabawę.

Wprowadźmy ponownie w terminalu komendę

$ node

a naszym oczom ukaże się konsola

>

Jak działa ? Identycznie jak ta z przeglądarki Chrome. Sprawdźmy:

> 4+5
9
> var a = 5;
undefined
> var b = 6;
undefined
> a+b
11
> a+c
ReferenceError: c is not defined
    at repl:1:4
    at REPLServer.self.eval (repl.js:110:21)
    at Interface. (repl.js:239:12)
    at Interface.EventEmitter.emit (events.js:95:17)
    at Interface._onLine (readline.js:202:10)
    at Interface._line (readline.js:531:8)
    at Interface._ttyWrite (readline.js:754:14)
    at ReadStream.onkeypress (readline.js:99:10)
    at ReadStream.EventEmitter.emit (events.js:98:17)
    at emitKey (readline.js:1089:12)
>

Oczywiście nie będziemy mieli z niej dużego pożytku, pisanie jednorazowych skryptów raczej nie jest naszym celem. Ok, jak więc odpalić uprzednio przygotowany?

$ node helloWorld.js

Po podaniu komendy podajemy ścieżkę. W naszym przypadku to tylko nazwa, ponieważ znajdujemy się w tej samej lokacji co plik.
Dobra, jedziemy dalej, sprawdźmy jaką zawartość chowa w sobie nasz skrypt helloWorld.js:

console.log("Dupa");

czego wynikiem będzie oczywiście…

Dupa

Fajnie!

Moduły i npm manager
Node i wszystko co go otacza ma budowę modułową. Co to właściwie oznacza ? W każdym osobnym skrypcie znajduje się zestaw funkcji i obiektów, które tworzą jedność i muszą zostać wyeksportowane. Jako przykładem możemy się tutaj posłużyć poznanym już wcześniej EventEmiterem czy wspomnianym powyżej modułem HTTP. Oczywiście w każdej chwili możemy stworzyć swój własny komponent, napisze więcej, bez tego to się raczej nie obejdziemy.
Jak się więc za to zabrać ? Pisząc podobną rzecz w przeglądarce, wypocilibyśmy coś takiego:

var calc = (function (window, undefined) {
    /**
    * @constructor
    **/
    function Calc () {
    }
    Calc.prototype.run = function (arg, operation, arg2, callback) {
        callback(this[operation](arg, arg2));
        return this;
    }
    Calc.prototype.add = function (arg, arg2) {
        return arg + arg2;
    }
    return new Calc;
}(window, undefined));

calc.run(1, 'add', 5, function (summary){
    console.log(summary);
});
Jeżeli powyższy kod wydaję Ci się zbyt zagmatwany i nie potrafisz zrozumieć co się tam właśnie stało, nie przejmuj się! Przewertuj dział JavaScript i odszukaj posty na temat podstaw.

A w przypadku modułu NodeJS będzie to wyglądać np. tak:

//Calc.js
/**
 * @constructor
 **/
function Calc () {

}

Calc.prototype.run = function (arg, operation, arg2, callback) {
    callback(this[operation](arg, arg2));
    return this;
}

Calc.prototype.add = function (arg, arg2) {
    return arg + arg2;
}

module.exports = new Calc;

Jak widać różnica niewielka, ważne są natomiast dwa aspekty. Każdy moduł posiada własną przestrzeń, zmienne zadeklarowane gdzieś indziej nie będą osiągalne w tym skrypcie. Druga sprawa, return został zastąpiony przypisaniem do module.exports, to właśnie w taki sposób będziemy informować Node’a, że w danym pliku znajduję się coś co chcemy wyeksportować. Warto nadmienić, że po pierwszym uruchomieniu aplikacji moduły będą automatycznie cachowane i każde ponowne odwołanie będzie odnosiło się do tego samego zapisanego wcześniej obiektu.
Pozostaje nam tylko zaimportować nasze cudo do skryptu głównego:

//Main.js
var calc = require('./Calc.js');

calc.run(1, 'add', 5, function (summary){
    console.log(summary);
});

Oraz odpalenie go w konsoli:

$ node Main.js
6

Do zaimportowania modułu posłużymy się metodą require, która, w tym przypadku, za argument przyjmuje ścieżkę do pliku.
Dobra, zanim dostaniemy zabawki musimy wiedzieć jeszcze jedną rzecz. Co to takiego jest npm i do czego służy?
Otóż nodeJS dostarcza nam bardzo kompleksowe narzędzie do zarządzania zewnętrznymi modułami, żeby być dokładnym, paczkami które je zawierają. I właśnie tym narzędziem jest npm (node package manager). Dzięki niemu możemy trzymać w ryzach wszystkie zależności niezbędne do uruchomienia naszej aplikacji. Oczywiście do wykonania całej operacji potrzebna nam będzie jeszcze odpowiednia konfiguracja projektu, która znajdować się będzie w pliku package.json:

//package.json
{
    "name": "project",
    "preferGlobal": "true",
    "version": "0.0.1",
    "author": "User name ",
    "description": "small description",
    "maintainers": [
        {
            "name": "User name",
            "email": "username@mail.com"
        }
    ],
    "repository": {
        "type": "git",
        "url": "https://github.com/user/demo.git"
    },
    "dependencies" : {
        "colors"   :  "*",
        "connect" : "2.6.x",
        "express" : "3.0.x",
        "jade" : "0.28.x",
        "socket.io" : "0.9.x"
    },
    "analyze": false,
    "devDependencies": {
        "vows"    :  "0.5.x",
        "request" :  "2.1.x"
    },
    "bundledDependencies": [
        "union",
        "ecstatic"
    ],
    "license": "MIT",
    "engines": {
        "node": ">=0.6"
    }
}

Jak rozwiązać teraz wszystkie zależności ? Wystarczy wejść do katalogu z projektem i wpisać odpowiednią komende:

$ npm install

a wszystko co opisaliśmy wcześniej zostanie pobrane i zainstalowane w odpowiednim miejscu.

W same szczegóły pliku package.json nie będziemy się jeszcze zagłębiać, przyjdzie na to czas i miejsce. Póki co skupmy się na celu który obraliśmy sobie w tej części kursu, czyli na stworzeniu serwera.

Serwer HTTP
Jak myślisz ile to może nam zająć ? Mając w głowie Apache’a czy ngix’a zadanie brzmi dość poważnie, tymczasem nic bardziej mylnego. Postawienie serwera z pomocą node’a to zaledwie parę linijek, a wszystko za sprawą modułu HTTP:

//Main.js
var http = require('http');
var onRequest = function (req, res) {
	res.writeHead(200, {'Content-Type': 'text/plain'});
	res.end('Dupa\n');
}
http.createServer(onRequest).listen(8080, '127.0.0.1');
console.log('Server running at http://127.0.0.1:8080/');

Rozszyfrujmy teraz co tak właściwie napisaliśmy.

var http = require('http');

Najpierw najważniejsze, w linijce pierwszej zaimportowaliśmy do naszego głównego skryptu instancję obiektu który zawiera natywny moduł HTTP.
Pozwoli nam to na korzystanie z wbudowanego API, dzięki któremu zbudujemy silnik pozwalający na przetwarzanie zapytań i wyświetlanie prostych stron.
Zauważ, że argument nie jest ścieżką, a nazwą modułu. Komponent HTTP jest wbudowany w jądro Node’a, więc jedyne co musimy zrobić to podać nazwę tego modułu.

http.createServer(onRequest).listen(8080, '127.0.0.1');

Powyższe wywołanie jest niemal identyczne jak wywołanie naszego prostego kalkulatora. Różnica jest taka, że przyjmuje tylko jeden parametr, callback który zostanie odpalony za każdym requestem. Aby serwer zaczął działać musimy go jeszcze uruchomić metodą listen, która za argumenty przyjmuje port i host.
Rzućmy jeszcze okiem na sam callback:

var onRequest = function (req, res) {
	res.writeHead(200, {'Content-Type': 'text/plain'});
	res.end('Dupa\n');
}

Przy każdym zapytaniu będziemy mieć do dyspozycji dwie zmienne:

  • req – zawiera parametry zapytania
  • res – zawiera obiekt odpowiedzi

To właśnie z pomocą tej drugiej przekażemy odpowiedni kod, nagłówki oraz treść którą ostatecznie wyświetlimy.

Szybki test:

$ node Main.js
Server running at http://127.0.0.1:8080/

Sprawdźmy zatem rezultat w przeglądarce. Co widzimy ? Dupę. No pięknie!
Rozszerzmy trochę funkcjonalność naszego serwera i zadbajmy o podstawowy routing.
Do tego zadania przyda nam się kolejny natywny moduł – URL. Dołączamy go do naszego skryptu dokładnie w ten sam sposób co wcześniej:

var url = require('url');

Następnie z jego pomocą sparsujemy ścieżkę:

    var pathname = url.parse(req.url).pathname,
        query = url.parse(req.url).query,

Url znajdujący się w obiekcie req (zapytania) początkowo jest tylko zwykłym stringiem, jednak po przepuszczeniu przez parser otrzymujemy obiekt z dwoma interesującymi nas właściwościami, pathname (to co znajduje się przed “?”) oraz query (reszta stringu). Dla adresu który wygląda tak:


http://127.0.0.1:8080/index?a=b

otrzymamy (w uproszczeniu) obiekt:

{
    "pathname" : "/index",
    "query" : "a=b"
}

Teraz wystarczy wykonać parę operacji na stringach i tablicach aby wyciągnąć ze zmiennej pathname nazwę akcji oraz przekształcić parametr query w tablice kluczy i wartości.

        paths = pathname.split("/"),
        route,
        _get = {};
    paths.reverse().pop();
    route = paths.pop();
    if (query) {
        query.split("&").forEach(function (el) {
            var arr = el.split("=");
            _get[arr[0]] = arr[1];
        });
    }

Zaczynamy od przecastiowania stringu do tablicy poprzez pocięcie go funkcją split, następnie deklarujemy zmienne route i get, przy czym ta druga będzie obiektem (w którym będziemy przetrzymywać klucze i wartości). W linijce nr 4 najpierw odwracamy kolejność tablicy za pomocą reverse, a następnie wyrzucamy pierwszy element (przykładowo mamy adres “/index/costam?a=b”, pierwsza wartość będzie zawsze pusta ponieważ przed pierwszym “/” nie mamy żadnych znaków). W podobny sposób poradzimy sobie ze zmienną query, o ile oczywiście będzie zawierała jakieś dane. Nowością może być użycie metody forEach. Jest to jedna z nowych funkcji obiektu tablicy dostępna w javascript’cie od wersji 1.6. Iteruje ona po każdym elemencie zwracając nam jako argument referencje do niego. W przeglądarce do tego typu operacji, aby zachować zgodność z starszymi przeglądarkami, użylibyśmy zapewne zewnętrznej biblioteki np takiej jak Underscore lub jQuery.

Jednym z elementów drugiej podstrony będzie formularz, aby go poprawnie wyświetlić (aby wyświetlić jakikolwiek tag html), musimy zmienić nagłówek “Content-Type” określający typ odpowiedzi.

    res.writeHead(200, {'Content-Type': 'text/html'});

Całość będzie wyglądać teraz następująco:

var http = require('http');
var url = require('url');
var onRequest = function (req, res) {
    var pathname = url.parse(req.url).pathname,
        query = url.parse(req.url).query,
        paths = pathname.split("/"),
        route,
        _get = {};
    paths.reverse().pop();
    route = paths.pop();
    if (query) {
        query.split("&").forEach(function (el) {
            var arr = el.split("=");
            _get[arr[0]] = arr[1];
        });
    }
    res.writeHead(200, {'Content-Type': 'text/html'});
    switch (route) {
        case '':
        case  'index' :
        {
            var response = "Dupa";
            if (_get.name) {
                response += " " + _get.name + "!";
            } else {
                response += "<a href='form'>Czyja?</a>";
            }
            res.end(
                response
            );
            break;
        }
        case 'form' :
        {
            var simpleForm =
                "<form action='/index' method='GET'>" +
                    "<label for='q'>No czyja? </label>" +
                    "<input id='q' type='text name='name' />" +
                    "<input type='submit' />" +
                    "</form>";
            res.end(simpleForm);
            break;
        }
        default :
        {
            res.end("404 not found!");
        }
    }
}
http.createServer(onRequest).listen(8080, '127.0.0.1');
console.log('Server running at http://127.0.0.1:8080/');

Czas zatem sprawdzić efekty naszej pracy poprzez ponowne załadowanie skryptu:

$ node Main.js
Server running at http://127.0.0.1:8080/

Wszystko działa ? Genialnie. W takim razie pozostała nam jeszcze jedna rzecz, obsłużenie danych przesłanych za pomocą POST-a.

Zanim się jednak za to zabierzemy, czas zrefaktoryzować trochę nasz kod:

/**
 * @param req
 * @returns {{}}
 *
 */
function getQueryParams(req) {
    // zwraca nam obiekt z wartościami przekazanymi GET-em
}

/**
 * @param req
 * @returns {string}
 *
 */
function getRoute(req) {
    // zwraca nam akcje na podstawie której zdecydujemy co zwrócić w odpowiedzi
}

/**
 * @param route
 * @param params
 * @returns {string}
 *
 */
function getResponse(route, params) {
    // funkcja przetwarzająca parametry i zwracająca odpowiedź
}

Jak widzimy powyżej, z całości wydzieliłem trzy funkcję getRoute, getQueryParams oraz getResponse, które będą odpowiadać kolejno, za akcje która zostanie wykonana, parametry które otrzymamy i za to jaka zostanie zwrócona odpowiedź.
Zmianie uległ także callback wykonywany po zarejestrowaniu zapytania.

var onRequest = function (req, res) {
    var data;
    res.writeHead(200, {'Content-Type': 'text/html'});
    if (req.method === "GET") {
        res.end(
            getResponse(
                getRoute(req),
                getQueryParams(req)
            )
        );
    } else if (req.method === "POST") {
        //obsługa post-a
    }
}

Bazując na wartości właściwości method obiektu req możemy sprawdzić czy zapytanie zostało wysłane GET-em albo POST-em. W przypadku tego pierwszego użyjemy wcześniej zdefiniowanych funkcji. Od razu nasuwa się nam pytanie czy w przypadku POST-a musimy szukać innego rozwiązania? Odpowiedź brzmi, jak najbardziej. Kolejne pytanie, dlaczego? W przypadku GET-a, parametry zaczytywane są bezpośrednio z URL-a, a całość przetwarzana jest synchronicznie (ten rodzaj zapytania może mieć bardzo ograniczony rozmiar), inaczej sytuacja wygląda w przypadku POST-a. Tutaj wielkość requesta zależy od limitów na serwerze. Node, aby zabezpieczyć się przed niepotrzebnym blokowaniem bufora wejścia dostarcza nam odpowiednie event-y, data oraz end.
Część kodu odpowiedzialna za obsłużenie tego requestu będzie wyglądać tak:

        //obsługa post-a
        req.setEncoding('utf-8');
        req.on('data', function (_data) {
            data += _data;
        });
        req.on('end', function() {
            data = qs.parse(data);
            res.end(
                //nasza odpowiedź
            );
        });


W linijce numer 2, ustawiamy kodowanie, gdybyśmy tego nie zrobili otrzymalibyśmy tylko ciąg znaków w postaci bitowej. Następnie (linijka 4) na event data dołączamy nową partie danych do obiektu który przechowuje całość zapytania. Musimy jeszcze uwzględnić, że ciąg danych który otrzymamy będziemy musieli odpowiednio zparsować (linia 7), niezbędny będzie nam do tego jeszcze jeden moduł:

var qs = require('querystring');


Na koniec wyświetlamy odpowiedź (linia 8), jednak dopiero wtedy gdy będziemy mieli całkowitą pewność że cały request został zarejestrowany (linia 6) na serwerze.
Dopiero w tym momencie możemy wywołać naszą metodę zwracającą poprawną odpowiedź:

//nasza odpowiedź
getResponse(
    getRoute(req),
    data
)


Zbierzmy wszystko w całość:

var http = require('http');
var url = require('url');
var qs = require('querystring');

/**
 * Zwraca nam obiekt z wartościami przekazanymi GET-em
 * @param req
 * @returns {{}}
 *
 */
function getQueryParams(req) {
    var query = url.parse(req.url).query,
        _get = {};
    if (query) {
        query.split("&").forEach(function (el) {
            var arr = el.split("=");
            _get[arr[0]] = arr[1];
        });
    }
    return _get;
}

/**
 * Zwraca nam akcje na podstawie której zdecydujemy co zwrócić w odpowiedzi
 * @param req
 * @returns {string}
 *
 */
function getRoute(req) {
    var pathname = url.parse(req.url).pathname,
        paths = pathname.split("/");
    paths.reverse().pop();
    return paths.pop();
}

/**
 * Zwraca odpowiedź
 * @param route
 * @param params
 * @returns {string}
 *
 */
function getResponse(route, params) {
    var response =
        '<html>' +
        '<head>' +
        '<title>Add something</title>' +
        '<meta charset="utf-8">' +
        '</head>' +
        '<body>';
    switch (route) {
        case '':
        case  'index' :
        {
            response += "Dupa";
            if (params.name) {
                response += " " + params.name + "!";
            } else {
                response += ". <a href='/form'>Kogo</a> lub <a href='/form-post'>czego</a>?"
            }
            break;
        }
        case 'form' :
        {
            response += "<p>" +
                "<form action='/index' method='GET'>" +
                "<label for='q'>No kogo? </label>" +
                "<input id='q' name='name'>" +
                "<input type='submit'>" +
                "</form>"
            "</p>";
            break;
        }
        case 'form-post' :
        {
            response += "<p>" +
                "<form action='/index' method='POST'>" +
                "<label for='q'>No czego? </label>" +
                "<input id='q' name='name'>" +
                "<input  name=submit type='submit'>" +
                "</form>"
            "</p>";
            break;
        }
        default :
        {
            response = "404 not found!";
        }
    }
    response +=
        '</body>' +
        '</html>'
    return response;
}

var onRequest = function (req, res) {
    var data = '';
    res.writeHead(200, {'Content-Type': 'text/html'});
    if (req.method === "GET") {
        res.end(
            getResponse(
                getRoute(req),
                getQueryParams(req)
            )
        );
    } else if (req.method === "POST") {
        //obsługa post-a
        req.setEncoding('utf-8');
        req.on('data', function (_data) {
            data += _data;
        });
        req.on('end', function() {
            data = qs.parse(data);
            res.end(
                //nasza odpowiedź
                getResponse(
                    getRoute(req),
                    data
                )
            );
        });
    }
}
http.createServer(onRequest).listen(8080, '127.0.0.1');
console.log('Server running at http://127.0.0.1:8080/');


Pozostaje nam jeszcze ponownie uruchomić serwer i sprawdzić czy wszystko jest w porządku. Być powinno.

Komentarze:
Peter
Odpowiedz
20/06/2017 w 11:45 AM

HARDCORE!

Zostaw odpowiedź

Proszę wypełnij wymagane pola aby dodać komentarz.


Drag circle to the rectangle on the right!

Wyślij