Kurs node.js #1

Node.js to w ostatnich latach jedna z najbardziej popularnych platform do tworzenia aplikacji internetowych. W prosty sposób, o ile znamy język JavaScript, możemy napisać w nim RESTowy serwer czy zwykłego bloga, że nie wspomnę już o różnych build systemach czy nieskomplikowanych skryptach I/O. Czas przyjrzeć się nieco bliżej temu zagadnieniu, budując niewielką stronę internetową.

Czym jest node.js?

Najprościej rzecz ujmując, node.js jest platformą, która pozwala uruchomić kod JavaScript. Wszystko dzięki temu, że zbudowana jest na tym samym silniku JS, który używany jest choćby w Google Chrome – mowa o V8. Sama możliwość uruchomienia skryptów JavaScript w innym środowisku niż przeglądarka byłaby jednak mało atrakcyjna. Siła Node.js tkwi w jego modułach. Oferuje on ich kilkadziesiąt, dzięki którym możesz zbudować bardzo ciekawe aplikacje. Co za tym idzie, masz np. do wyboru moduł HTTP, dzięki którym obsłużysz wszystkie zapytania HTTP, tworząc serwer www. Dostępny jest też moduł File System, dzięki któremu dostaniesz się do systemu plików. Dzięki Bufferowi obsłużysz dane binarne. I tak dalej. Jest tego naprawdę sporo i warto się temu przyjrzeć. Gwoli sprawiedliwości należy dodać, że dokumentacja node.js to pięta achillesowa tego projektu, choć pocieszać można się, że na początku było z tym jeszcze gorzej.

Co więcej, dzięki globalnej bibliotece modułów npm, każdy może łatwo zainstalować niestandardowe moduły np. poprzez npm install nazwaModulu, jak i również dodać swoje produkcje do niej.

Zaczynajmy!

Przede wszystkim musimy zainstalować node na własnym komputerze, tak jak zapewne robiłeś to w przeszłości ze swoim serwerem Apache, ngix, PHP, Ruby i tak dalej. W tym celu należy wejść na nodejs.org. Wszystkie potrzebne źródła znajdziesz na podstronie Download. Po wykonaniu niezbędnych czynności powinieneś być w stanie odpalić terminal/konsolę w swoim systemie i wpisać node -v.

Wow, działa!

Pierwszy kod

Powiedzieliśmy sobie, że node.js interpretuje pliki JS. Stwórzmy więc jakiś przykładowy i odpalmy go używając node’a.

Mój plik example.js wygląda tak:

console.log("hello world!");

Uruchamiamy go w terminalu poprzez wpisanie node example.js (będąc w folderze, w którym jest plik example.js):

Wygląda na to, że node wspiera konstrukcję console.log, z reguły służącą nam do debugowania skryptów dla przeglądarek. Szybko wypróbujmy jeszcze parę podstawowych operacji:

var a = 1;
var b = 2;
var obj = {
	c: 3
};
console.log(a + b + obj.c); // 6

Świetnie, pora przejść do nieco bardziej interesujących przykładów. Pamiętaj, że za każdym razem, gdy aktualizujesz swoje pliki JS musisz odpalić node’a na nowo. Sprawę ułatwiają specjalne narzędzia typu forever, aczkolwiek porozmawiamy o nich nieco później.

Serwer w node.js

Od wieków, o ile nie od początku, na głównej stronie node.js “wisi” przykładowy kod, który pozwala szybko uruchomić własny serwer na node.js:

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

Wypróbujmy go, stosownie aktualizując nasz example.js:

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

Jak go uruchomić? Podobnie do poprzedniego przykładu, zaglądamy do terminala i voilà!

Zaglądamy teraz do przeglądarki pod adresem http://127.0.0.1:1337:

Wynika na to, że odpaliliśmy swój własny serwer HTTP i serwujemy prostą stronę w czystym tekście.

Jak to się stało? Przyjrzyjmy się.

Po pierwsze bardzo ważna jest pierwsza linijka:

var http = require('http');

W ten sposób do zmiennej http ładujemy natywny moduł HTTP z biblioteki modułów node.js, o których wspomniałem co nieco we wstępie. Pozwoli to nam na korzystanie z bardzo prostego API, dzięki któremu zbudujemy silnik do serwowania prostej strony internetowej.

Następnie całą magia leży w poniższym kodzie:

http.createServer(function (req, res) {
	res.writeHead(200, {'Content-Type': 'text/plain'});
	res.end('Hello World\n');
}).listen(1337, '127.0.0.1');

Uruchamiamy metodę createServer z obiektu http. Jako argument podajemy funkcję, którą możemy nazwać callbackiem. To znaczy tyle, ze za każdym requestem do naszej strony odpali się on mając do dyspozycji dwie zmienne:

Najprościej rzecz ujmując, jeśli użytkownik wpisuje nasz adres (u nas http://127.0.0.1:1337 ) i klika enter, request HTTP (zmienna req) wychodzi do naszego serwera, na który należy odpowiedzieć (dlatego też zmienna res, która posłuży do odpowiedzi nań).

Odpowiadamy więc na żądanie HTTP najpierw wysyłając nagłówek HTTP o kodzie 200 typu Content-Type. Oznacza on, jakiego… typu dane wysyłamy w odpowiedzi. Wartością tego nagłówka jest text/plain, czyli wyślemy czysty tekst. Możliwe do wysłania nagłówki HTTP znajdziesz choćby na Wikipedii.

Zauważ, że w writeHead drugim parametrem jest obiekt. Możemy więc wysłać kilka nagłówków naraz:

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

I tak dalej.

Na żadanie HTTP ostatecznie odpowiadamy metodą end, przesyłając treść odpowiedzi jako parametr:

res.end('Hello World\n');

Ostatnią sprawą, na którą musimy zwrócić uwagę, jest użycie funkcji listen:

listen(1337, '127.0.0.1')

Pierwszy argument to port, na którym stać ma serwer, a drugi to host. A więc adresem naszego serwera jest zbitka tych dwóch wartości 127.0.0.1:1337. Warto wypróbować różne wartości portów i hostów.

Zauważ, że listen jest użyty w chainingu, czyli po kropce od razu po użyciu createServer. Równie dobrze kod może wyglądać tak:

var http = require('http');
var server = http.createServer(function (req, res) {
	res.writeHead(200, {'Content-Type': 'text/plain'});
	res.end('Hello World\n');
});

server.listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');

Co by tu zmienić?

Spróbujmy teraz zamiast tekstu zaserwować stronę HTML. Najpierw zmieńmy odpowiedź:

res.end('<html><head><title>Foobar</title></head><body><h1>Hello world!</h1></body></html>');

Rezultat w przeglądarce jest następujący:

Coś poszło nie tak… Dlaczego widzimy źródło strony, zamiast Hello world!? Wina leży oczywiście po stronie node’a, a mianowicie złego nagłówka HTTP. Wysyłamy bowiem odpowiedź jako tekst, a nie jako html. Poprawmy to:

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

Całość zaczęła działać jak należy!

Wyślijmy formularz!

Skorzystaliśmy już z modułu HTTP i postawiliśmy prosty serwer. Co dalej? Spróbujmy na przykład stworzyć stronę z formularzem i wyświetlić otrzymane z niego dane.

Zaczynamy mając do dyspozycji plik example.js, zbudowany następująco:

var http = require('http');
var server = http.createServer(function (req, res) {
	res.writeHead(200, {'Content-Type': 'text/html'});
	res.end('Foobar

Hello world!

'); }); server.listen(1337, '127.0.0.1'); console.log('Server running at http://127.0.0.1:1337/');

Musimy stworzyć prosty HTML dla naszej strony, zawierający formularz.

var pageHTML = '<html>' +
					'<head>' +
						'<title>Add something</title>' +
						'<meta charset="utf-8">' +
					'</head>' +
					'<body>' +
						'<form method="post" action="">' +
							'<div>' +
							    	'<label for="nickname">Nickname:</label>'+
							    	'<input type="text" name="nickname">' +
							'</div>' +
							'<div>' +
							    	'<input type="submit" value="send it">' +
							'</div>' +
						'</form>' +
					'</body>' +
				'</html>';

Pora wyświetlić taką stronę:

var http = require('http');

var pageHTML = '<html>' +
					'<head>' +
						'<title>Add something</title>' +
						'<meta charset="utf-8">' +
					'</head>' +
					'<body>' +
						'<form method="post" action="">' +
							'<div>' +
							    	'<label for="nickname">Nickname:</label>'+
							    	'<input type="text" name="nickname">' +
							'</div>' +
							'<div>' +
							    	'<input type="submit" value="send it">' +
							'</div>' +
						'</form>' +
					'</body>' +
				'</html>';
                        

var server = http.createServer(function (req, res) {
	res.writeHead(200, {'Content-Type': 'text/html'});
	res.end(pageHTML);
});

server.listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');

Wszystko gra, a sprawę załatwiła res.end(pageHTML);:

Pora wprowadzić pewne modyfikacje, które pomogą nam dostać się do danych POST w requeście.

POST vs GET

Kiedy wchodzimy na stronę przez przeglądarkę, wysyłamy żądanie GET. Kiedy wysłamy formularz – “leci” żądanie POST. Trzeba obsłużyć te dwa przypadki. A więc najpierw zajmijmy się GETem:

var server = http.createServer(function (req, res) {
	if (req.method === "GET") {
		res.writeHead(200, {'Content-Type': 'text/html'});
		res.end(pageHTML);
	}
});

Skorzystaliśmy z parametru req.method, który oznacza metodę, z jaką zostało wysłane żądanie HTTP. Inne tego typu metody to oczywiście POST, PUT i DELETE.

Musimy teraz zaimplementować przypadek, kiedy ktoś wysyła request typu POST:

var server = http.createServer(function (req, res) {
	if (req.method === "GET") {
		res.writeHead(200, {'Content-Type': 'text/html'});
		res.end(pageHTML);
	} else if (req.method === "POST") {
		console.log("wysyłam dane postem");
	}
});

Pojawia się pytanie, skąd wziąć dane, które przesłał nam użytkownik w formularzu? Należy skorzystać ze specjalnego zdarzenia data, które odpala się, kiedy dochodzi do nas stream danych wraz z requestem HTTP. Sklejamy więc te dane w string.

var server = http.createServer(function (req, res) {
	var requestData = '';
	if (req.method === "GET") {
		res.writeHead(200, {'Content-Type': 'text/html'});
		res.end(pageHTML);
	} else if (req.method === "POST") {
		req.on('data', function(data) {
			requestData += data;
		});
	}
});

Ciekawą rzeczą jest to, jakie dane doklejamy do zmiennej requestData. Sprawdźmy, co w ogóle przychodzi do nas pod zmienną data:

req.on('data', function(data) {
	console.log(data);
	requestData += data;
});

Podpatrując w terminalu, po wysłaniu formularza widzimy coś takiego:

Dostajemy jakiś ciąg znaków w czystej bitowej postaci. Możemy od razu stwierdzić, że chcemy, by to był string, używając req.setEncoding(‘utf-8’);:

var server = http.createServer(function (req, res) {
	var requestData = '';
	if (req.method === "GET") {
		res.writeHead(200, {'Content-Type': 'text/html'});
		res.end(pageHTML);
	} else if (req.method === "POST") {
		req.setEncoding('utf-8');
		req.on('data', function(data) {
			requestData += data;
		});
	}
});

Pora teraz wyświetlić te dane i dać odpowiedź na żądanie HTTP. Jako że danych może być wiele i nie wiadomo, kiedy skończy się ich streaming, musimy użyć zdarzenia end, które zakomunikuje nam, że to najwłaściwsza pora na odpowiedź, ponieważ request na nią czeka.

var server = http.createServer(function (req, res) {
	var requestData = '';
	if (req.method === "GET") {
		res.writeHead(200, {'Content-Type': 'text/html'});
		res.end(pageHTML);
	} else if (req.method === "POST") {
		req.setEncoding('utf-8');
		req.on('data', function(data) {
			requestData += data;
		});
		req.on('end', function() {
					  
		});
	}
});

Super. Ostatnia rzecz to wyświetlenie strony HTML z danymi otrzymanymi z POST.

Żeby to zrobić należy uczynić coś ze zmienną requestData. Wiemy, że jest równa np. stringowi nickname=foobar. Chcielibyśmy przekonwertować tego stringa do jakiejś bardziej JSowej postaci, na przykład obiektu. Nic prostszego. W tym celu należy użyć modułu querystring. A więc na początku pliku “pobieramy” go do zmiennej:

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

Mając go pod zmienną qs, spokojnie możemy go użyć w zdarzeniu end:

req.on('end', function() {
	var postData = qs.parse(requestData);
  	console.log(postData.nickname); // np. "foobar"
});

Ostatnia rzecz to wyświetlenie odpowiedniej strony i ostateczna odpowiedź na request:

req.on('end', function() {
	var postData = qs.parse(requestData);
	res.writeHead(200, {'Content-Type': 'text/html'});
	res.end('Your nickname

Your nickname is: '+ postData.nickname + '

'); });

To wszystko! W przeglądarce wygląda to tak:

Finalny kod prezentuję się zaś następująco:

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

var pageHTML = '<html>' +
					'<head>' +
						'<title>Add something</title>' +
						'<meta charset="utf-8">' +
					'</head>' +
					'<body>' +
						'<form method="post" action="">' +
							'<div>' +
								'<label for="nickname">Nickname:</label>'+
								'<input type="text" name="nickname">' +
							'</div>' +
							'<div>' +
								'<input type="submit" value="send it">' +
							'</div>' +
						'</form>' +
					'</body>' +
				'</html>';
						

var server = http.createServer(function (req, res) {
	var requestData = '';
	if (req.method === "GET") {
		res.writeHead(200, {'Content-Type': 'text/html'});
		res.end(pageHTML);
	} else if (req.method === "POST") {
		req.setEncoding('utf-8');
		req.on('data', function(data) {
			requestData += data;
		});
		req.on('end', function() {
			var postData = qs.parse(requestData);
			res.writeHead(200, {'Content-Type': 'text/html'});
			res.end('<html><head><title>Your nickname</title></head><body><h1>Your nickname is: '+ postData.nickname + '</h1></body></html>');			  
		});
	}
});

server.listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');

PS Warto zabezpieczyć się przed złośliwymi atakami, gdy ktoś będzie chciał wysłać zbyt wiele danych POST. Na przykład w ten sposób:

req.on('data', function(data) {
	requestData += data;
	if (requestData.length > 1e6) {
		requestData = "";
		res.writeHead(413, {'Content-Type': 'text/plain'});
		req.connection.destroy();
	}
});

Epilog

Zbudowaliśmy właśnie prosty serwer node z obsługą żądań POST. Nadal jednak nasza aplikacja jest bardzo prosta, piszemy “z ręki” kod HTML, nie zapisujemy nigdzie danych, a nasz kod nie jest podzielony na moduły, że o walidacji danych POST nie wspomnę. Zrobimy to w następnych odcinkach.

Komentarze

1

Jak rozumiem, zabezpieczenie polega na sprawdzeniu długości żądania i zwróceniu błędu 413, gdy jest ich za dużo i zamknięciu połączenia?

Dzięki i czekam na więcej. W sumie podchodziłem do node’a parę razy i zawsze mi brakowało samozaparcia – a tu przynajmniej przystępnie opisane. :)

2

Dokładnie.

3

Bardzo fajny tutorial. Mam jedno pytanko nie związane z tematem. Czy jest możliwość zaimplementowania lepszego parsera dla wklejanego kodu? Zdecydowanie łatwiej by się go wtedy czytało.

Pozdrawiam

artur
4

Fajny tutorial, ale skoro już zabezpieczamy przed przydługim POSTem, to myślę, że poważniejszym błędem jest brak zabezpieczenia przed Cross Site Scripting. W tym przykładzie wystarczy, że jako nickname podamy (nawiasy ostre zamiast kwadratowych:

[img src=x onerror=alert(1)]

i można wykonać dowolny kod JS w kontekście załadowanej strony.

Przed wyświetleniem zmiennej nickname trzeba ją encodować np tak: http://stackoverflow.com/a/4419541

5

Oczywiście Krzysztof. Dobra uwaga, nie chciałem aż tak skupiać się na tym – w kolejnych odcinkach napiszę o tym więcej. :)

6

Bardzo dobrze się zapowiada, z Twojej stronki nauczyłem się też JQuery.
Może napiszesz coś o frameworkach do node.js?

expert
7

Bóg Ci w dzieciach wynagrodzi za ten kurs!

yonki
8

super! czekam na ciąg dalszy

Arvangen
9

yonki, im więcej Damian będzie miał dzieci tym mniej czasu na robotę (w tym tworzenie kursów), także proponuję uważać z tego typu komentarzami ;)
Rozważam właśnie node albo pythona do swojego małego build systemu (chodzi głównie o mielenie templatek z PHP w środku).

Dzięki za wpis :)

10

Bardzo ciekawy wpis. Szkoda tylko, że tak krótki. Czekam z niecierpliwością na więcej :)

dev
11

Od jakiegoś czasu próbuję zrozumieć ideę tego całego boomu node.js, backbone.js, ember.js itd. Im dłużej czytam, tym większy mam mentlik w głowie.
Z moich obserwacji wynika że jesteśmy w stanie po prostu uruchomić JS poza przeglądarką. Gdzieś tam pojawiają się kwestie szybkości itd. Jak to się ima do starego rękodzieła w PHP? Jedno ma zastąpić drugie, jak wygląda zastosowanie?

kilogram
12

a jak z hostingiem? są jakieś darmowe (nodester?) ale czy można coś na tym postawić? chciałbym się pobawic Node.js ale bez sensu inwestować w hosting

Zre
13

Kiedy następna część i czy w ogóle jest jeszcze w planach?

wisnia93
14

Witam!
Początek zachęcający ale wali mi sie kiedy autor pisze:
Musimy stworzyć prosty HTML dla naszej strony, zawierający formularz.
Co to ma być? PLIK jesli tak to o jakiej nazwie, jesli nie nowy plik to GDZIE to wstawić.
Proszę o podpowiedz
Pozdrawiam
Jacek

jack
15

@jack:
Właśnie to, co poniżej tego zdania, to jest “stworzenie prostego HTMLa”; Po prostu tworzymy zmienną pageHTML, do której wpisujemy tekst, zawierający znaczniki HTML. Przecież itd. to HTML.

Robert Skarżycki
16

i jak tam? Są/będą jakieś następne części bo coś szukam i nie ma :/

Piotrek
17

Też się niecierpliwię? Będzie coś?

18

Spoko wpis, tylko czytam czytam i koniec, proszę o więcej :D

19

Kiedy nastąpi te wkrótce? :)

man
20

dzięki!

Dodaj komentarz

Dozwolone tagi: <blockquote>, <code>, <strong>