Node.js – EventEmitter

Razumevanje EventEmitter-a u Node.js

Node.js je popularna platforma koja omogućava izgradnju skalabilnih aplikacija kroz asinhroni, događajno vođeni pristup. Jedan od ključnih mehanizama koji omogućava ovaj pristup jeste EventEmitter. Da bismo razumeli njegovu važnost i funkcionalnost, zamislimo EventEmitter kao središte za komunikaciju unutar Node.js aplikacije, gde se događaji šalju, primaju i obrađuju.

Šta je EventEmitter?

EventEmitter je klasa unutar Node.js events modula koja omogućava objektima da emituju događaje i reaguju na njih. To je temelj na kojem Node.js gradi svoj asinhroni, događajno vođeni model programiranja. Možemo ga zamisliti kao sistem za razmenu poruka, gde se poruke (događaji) šalju i obrađuju u realnom vremenu.

megafon

Kako EventEmitter funkcioniše?

Predstavite EventEmitter kao radijsku stanicu koja emituje razne programe (događaje). Slušaoci (handlers ili listeners) se “prijavljuju” da slušaju određene programe registracijom funkcija koje će biti pozvane kada se određeni događaj emituje. Kada radio stanica emituje program, svi registrovani slušaoci za taj program automatski “reaguju” – u našem slučaju, funkcije se izvršavaju.

Primeri upotrebe

EventEmitter se široko koristi unutar Node.js ekosistema. Na primer, u web serverima kreiranim pomoću Node.js-a, EventEmitter se koristi za obradu HTTP zahteva. Kada server primi zahtev, emituje se događaj, a odgovarajući listeneri reaguju na taj događaj, omogućavajući asinhronu obradu zahteva.

Kreiranje i korišćenje EventEmitter-a

Da bismo koristili EventEmitter, prvo ga uvozimo iz events modula. Zatim kreiramo instancu EventEmitter-a. Na ovoj instanci možemo emitovati događaje i registrovati slušaoce koji će reagovati na te događaje. Primer koda ispod ilustruje osnovnu upotrebu:

Ovaj dogadjaj može da se “osluškuje” tako što će se “emitter objekat” prijaviti/registrovati na njega:

Ukoliko se “opaljuje” više istih dogadjaja jedan za drugim a nama je iz nekog razloga potrebno da se samo jednom reaguje na dogadjaj, to se postiže korišćenjem metode once():

Kada koristimo “once” metodu onda će naš sistem samo jednom reagovati na ovaj tip poruke:

Zaključak

EventEmitter je snažan alat u Node.js koji omogućava efikasnu komunikaciju i obradu događaja unutar aplikacija. Kroz događajno vođeni model, aplikacije mogu efikasno reagovati na različite ulazne podatke i signale, što doprinosi visokoj skalabilnosti i performansama. Razumevanje i efikasna upotreba EventEmitter-a ključni su za izgradnju robustnih Node.js aplikacija.


Kreiranje servera sa Node.js

Kreiranje server objekta

node.js server

Za kreiranje servera je potreban http/https objekat koji je ugradjen u core node.js-a te ga možemo importovati kao modul. Postoje dve opcije modula:

  • http – Http je jedan od ugrađenih modula koji dolazi sa Node.js i omogućava kreiranje servera za prihvatanje zahteva od klijenta i vraćanje odgovora
  • https – Https modul uključuje sve osnovne karakteristike http-a, ali sa dodatnim opcijama za rukovanje potrebnim bezbednosnim razlikama poput sertifikata.

Kada importujemo jedan od ova dva modula, on će nam vratiti “http/https” objekat, kao u sledećem primeru:

Sada kada imamo http objekat iskoristićemo ga za kreiranje servera koristeći metodu createServer():

Da bi server započeo “osluškivanje” zahteva moramo ga startovati, to se radi pozivanjem metode listen():

Metoda listen() prihvata četri opciona parametra, a mi ćemo koristi samo prvi parametar koji definiše port:

Zahtev ka serveru se upućuje sa ove adrese "http://localhost:3000". Za zaustavljanje rada server možemo korisiti njegovu metodu close().

Callback za reagovanje na zahtev tzv. requestListener()

U prethodnom primeru je kreiran server ali nema nikakvu funkcionalnost, da bi ovaj server ipak reagovao na neki zahtev potrebno je da mu se kreira neki event listener koji će osluškivati zahteve ka serveru. To možemo uraditi na standardni tzv. event-based način sa metodom on():

Medjutim ovakav pristup se najčešće ne koristi (mada je potpuno validan), već se metodi createServer() kao parametar prosedjuje funkcija (tzv. requestListener()) koja reaguje na zahtev i koja sama automatski dodaje “request” event.

zbog jednostavnosti najbolje je koristi anonimnu funkciju:

Prihvatanje GET zahteva i odgovor

Prihvatanje zahteva

Uz zahtev serveru se šalje request objekat i njemu možemo da pristupimo kroz parametar “request”. To je prilično “veliki” objekat koji nosi puno informacija u svojim svojstvima i metodama, pa ih možemo iskoristi za sadržaj body-ja odgovora (npr. svojstva method, url, headers):

Parsiranje query-ja iz url adrese

Ukoliko je zahtev pošiljaoca specifičan on će ga proslediti kroz url (npr. http://host:8000/?name=Pera), te je stoga neophodno parsirati url i izvući podatke iz njega. Za parsiranje ćemo koristi node.js-ov modul “url”.

Odgovor servera

a) API based odgovor

Za kreiranje osnovnog odgovora (response) ćemo koristi svojstva response objekata: statusCode i njegove metode: setHeader(), write() i end():

Za odgovor servera možemo iskoristi neke od podatak koje dobijamo iz request objekta:

U prethodnom primeru je definisan najjednostavniji header odgovora, ovde možete pogledati kako može da izgleda standardni header.

NAPOMENA:
U prethodnom primeru možemo iskoristili prednosti koje donosi destrukturiranje objekta tako da veoma jednostavno kreiramo promenjive headers, method i url. Pogledajte ovde kako bi izgledao prethodni primer kada bi se koristilo destruktuiranje objekta, više o destrukturiranju objekta pogledajte u članku “Destruktuiranje u JavaScriptu”

b) HTML odgovor

U prethodnim primerima su odgovori (response) bili tzv. “API based”, ali ako hoćemo da vratimo html onda ćemo to uraditi slično sledećem primeru:

Ako je u prethodnom primeru url sa kojom pošiljalac šalje zahtev tipa http://localhost:3000/?name=Pera, onda će pošiljalac dobiti odgovor “Zdravo Pera” u suprotnom će dobiti odgovor “Hello World”.

Primanje podataka poslatih sa POST/PUT metodom

Pored get metode koja vidljivo šalje podatke u okviru url-a, možemo “nevidljivo” poslalti podatke serveru ukoliko koristimo POST metodu. Obično se ovaj metod koristi kada šaljemo prikupljene podatke iz forme. U ovome primeru formu ćemo generisati na serveru kada klijent pošalje zahtev za stranicu sa url-om: http://localhost:3000:

Rezult ovog zahteva na klijentu ovako izgleda:

forma POST

Kada korisnik unese podatke u formu i klikne na dugme “Save”, zbog definisanog HTML atributa “action” će se podaci iz forme poslati na stranicu sa url-om /message, i uz njega će se kreirati novi zahtev koji će proslediti podatke. Prikupljeni podaci iz forme se šalju kao “readable streams” i mogu izgledati ovako:

Šta su STREAMS?
Streams su način za efikasno rukovanje bilo kojom vrstom razmene informacija (koristi se takodje za čitanje/pisanje datoteka, mrežnom komunikacijom…). Streams nisu koncept jedinstven samo za Node.js, već postoje u okviru operativnog sistema Unix više decenija. Za razliku od tradicionalanog načina gde se datoteka cela učita u memoriju pa se tek onda obrađuje, ovde pri korišćenju streams program čita deo po deo podataka i simultano ga obrađuje, te na taj način uopšte ne opterećuje memoriju. Pored pomenute prednosti gde se efikasno koristi memorija jer ne trebate učitavati velike količine podataka u memoriju da biste mogli da ih obradite, postoji i druga prednost tzv. “vremenska efikasnost” koaj smanjuje potrebno vremena za početak obrade podataka, jer obradu možete započeti čim dobijete deo podataka, umesto da čekate da budu svi podaci budu dostupni. Postoje tri vrste stream-a:

  • “writable stream” (omogućavaju node.js da prebaci podatke u stream)
  • “readable stream” (omogućava node.js da čita podatke iz stream-a)
  • “duplex” (omogućava node.js i da piše i da čita stream)

Da bi znali u kome trenutku se šalju podaci i kada je prenos podataka završen za to postoje registrovani dogadjaji. “Readable streams” imaju registrovane sledeće tipove dogadjaja:

  • data (ovaj dogadjaj se emituje kad god stream prosledi deo podataka tzv. “chunk”)
  • end (ovaj dogadjaj se emituje kad god stream nema više podataka da prosledi)
  • error
  • close
  • readable

U sledećem primeru ćemo se prijaviti na “data” dogadjaj za obaveštenje u kome trenutku se šalju podaci, a da bi smo znali kada je kraj poruke takodje se moramo prijaviti i na “end” dogodjaj:

Promenjiva body bi po prijemu podataka mogla ovako izgledati:

Kada imamo podatke u ovome obliku potrebno je da ih parsiramo, a za to ćemo koristi node.js-ov modul “querystring” i njegovu metodu parse() koja će podatke u ovom obliku prebaciti u kolekciju “key/value” parova:

U konzoli će biti štampan sada nama čitljiv kod:

Ovde može te pogledati ceo prethodni primer objedinjen kao i primer u kome se concat-uje buffer (chunk) u niz.

×

×

Destruktuiranje objekta request:

×

Primer response header-a:

×

Primer concat-ovanja bufera

×

Primer


Uvod u Node.js

Šta je node.js?

JavaScript je programski jezik koji ne može da se samostalno koristi već je za njegovo izvršavanje potrebno specijalno okruženje tzv. “JavaScript runtime environment”. Najpoznatiji “JavaScript runtime environment” je browser, ali ukoliko želimo da izvršavamo JavaScript i van browser-a tu na scenu nastupa node.js kao drugi “JavaScript runtime environment”. Node.js je open-source, cross-platform JavaScript runtime environment koji se bazira na Chrome-ovom V8 JavaScript engine-u.
Pošto je Node.js tzv. “low-level” platforma za jednostavniju upotrebu je pametno koristiti neki od mnogobrojnih framework-a kao što su: Express, Koa, Total.js, Sails….

Node.js je JavaScript runtime environment i omogućava rad sa JavaScript-om i van browser-a!

Node.js teži neblokirajućem i asinhronom načinu programiranja i u isto vreme može da obavlja više stvari odjednom, za razliku od kombinacije PHP/Apache koja funkcioniše putem zahteva i odgovora. PHP/Apache server obrađuje zahtev za datotekom tako što šalje task u file system računara, tu sačeka dok se sistem datoteka otvori i pročita datoteku, a zatim kao rezultat vraća traženi sadržaj klijentu. Tek nakon ovoga je spreman da odgovori na sledeći zahtev.

Kada Node.js obrađuje zahtev za nekom datotekom, on šalje task u file system računara i odmah nakon toga je spreman da prihvati na sledeći zahtev. U medjuvremenu file system otvori i pročita datoteku, nakon čega server vraća sadržaj klijentu. Upravo ovo je najveća prednost Node.js servera, jer eliminiše čekanje i jednostavno nastavlja sa sledećim zahtevom. UI vođen-događajima ga čini savršenim za aplikacije koje rade u realnom vremenu i iziskuju veće količine prenosa podataka. Node.js je baziran na tome da uglavnom veći deo koda samo sedi u pozadini i čeka da se desi neki I/O (Ulaz/Izlaz), kao što su čekanje da se fajl upiše na disk ili da MySql upit vrati podatke. Kada zahtevamo da se otvori fajl, ne čekamo na rezulat nego kažemo kojoj funkciji da prosledimo podatke kad je citanje gotovo ili koji događaj da pozove, a izvršavanje drugog koda se nastavlja. Više o “JavaScript runtime environment” možete pogledati u članku “Simbioza JavaScripta i njegovog okruženja”.

Razlike izmedju Node.js i Browser-a

Pored to toga što obe platforme omogućavaju izvršavanje JavaScript-a takodje postoje i razlike medju njima:

  • Node.js može da kreira server – Node.js ima ugrađene biblioteke za HTTP i socketkomunikaciju sa kojima može da kreira web server i tako bude zamena za druge tipove servera kao npr. Apache, Nginx…
  • Node.js koristi CommonJS sintaksu sa kojom omogućava modularno programiranje – Iako u okviru ES6 postoji sitaksa za rad sa modulima tzv. import/export sintaksa, ona je u node.js-u tek odnedavno ubačena i to kao eksperimantalna opcija. Za rad sa modulima Node.js koristi CommonJS sintaksu (više o ovome možete pročitati u članku Modularno programiranje – eksterna sintaksa).
  • Node.js nema DOM, window objekat, document objekat, cookies… – Node.js nema API-je pretraživača vezane za DOM, CSS, performansame, document, kao i sve API-je povezane za “window” objekat. Upravo zbog nedostatka window objekta koji predstavlja globalni objekat dostupan svima, u Node.js-u takodje postoji novi globalni objekat koji je dostupan su u svim modulima i ne moramo da ga uključujemo specijalno u našu aplikaciju, već ga možemo direktno koristiti svuda. Ovaj globalni objekat sadrži mnoštvo korisnih svojstava okruženja kao: setImmediate(), setInterval(), clearTimeout(), console i process. Ovaj objekat možemo da pregledamo jednostavnim izlistavanjem u konzoli sa:

  • Napomena:
    Sledeće promenljive (__dirname, __filename, exports, require(), module) izgledaju kao da su deo globalne promenjive, ali nisu, one postoje kao deo modula.

  • Global namespace object – U Browseru, opseg najvišeg nivoa je globalni opseg, što znači da će ključna reč “var” unutar browser-a definisati novu globalnu promenljivu. U Node.js-u je ovo drugačije, jer opseg najvišeg nivoa nije globalni opseg, pa će ključna reč “var” unutar nekog modula Node.js-a biti dostupna samo za taj modul.
  • Node.js ima pristup fajlovima sistema u kome se nalazi – Node.js ima puni pristup sistemu kao i bilo koja druga izvorna aplikacija. To znači da može čitati i pisati direktno u ili iz sistema datoteka, takodje može imati neograničen pristup mreži, i može izvršavati softver…
  • Moderan JS u okviru Node.js – Budući da se razvoj JavaScript-a kreće veoma brzo, pregledači često kaskaju sa implementacijuom novih mogućnosti JavaScript-a tako da u Browser-u morate da koristite stariju verziju JavaScript-a, ali to ne važi za Node.js u njemu možete koristiti sav moderni ES6-7-8-9 JavaScript koji vaša verzija Node.js podržava.

Nedostaci Node.js-a

Node.js izvršava kod na jednom thread-u (niti), i to je ustvari jedna od najvećih prednosti u odnosu na druge opcije koje podržavaju multi-threading, jer upravo jedno-nitna priroda čini njegove programe izuzetno skalabilnim.

Šta znači da je aplikacija skalabilna?

Skalabilana aplikacija je ona koja sa istim kodom može obraditi mnogo veći broja nekih zahteva nego što je prvobitno planirano a bez trošenja dodatnih resursa, što nije slučaj kod multi-thread tipa aplikacije jer pri povećavanju obima posla dolazi do povećavanja potrošnje sistemskih resursa (RAM…). Više o radu Event loop-a možete pročitati u članku “Princip rada asinhronog JavaScript-a”

Upravo pomenuta prednost je i najveći nedostatak Node.js-a jer zbog takve prirode node.js-a, on nema mogućnost da neblokirajući obrađuje zadatke vezane za CPU. Objasnićemo ovaj problem kroz primer:
Kad Node.js primi zadatak vezan za CPU, on sav CPU resurs isporučuje tom zadatku i uzima da ga odmah obradi, a tek po završetku odgovara na druge zahteve u loop-u što dovodi do spore obrade i kašnjenja u “event loop-u”.

Da li postoji rešenje za ovaj problem?

U novijim Node.js verzijama (>10.5.0) se pojavio novi modul pod nazivom “Worker threads” koji omogućava postojanje više paralelnih niti za izvršavaje JS-a, što pomaže u izvođenju CPU intenzivnih JS operacija. Međutim i dalje postoje odredjeni uslovi jer se “Worker threads” mogu izvoditi samo na mašinama sa više jezgara (jer Node.js omogućava upotrebu samo jedne niti po jezgru), a ne može se preskočiti ni činjenica da je ova funkcija i dalje eksperimentalna u trenutno aktivnim Node.js verzijama.


Instalacija node.js

Da bi instalirali node.js na lokalnoj mašini neophodno da se imamo instaliran Python 2.7, pa tek onda možemo download-ovati node.js (npr. odgovarajuću windows installer verziju “.msi”) sa https://nodejs.org/download stranice.
Instalacija je standardna a po završetku se dobijaju sledeće stvari:

  • node.js
  • dokumentacija za node.js
  • node.js command prompt
  • npm package manager

Provera instaliranih verzija node.js i njegovog package manager-a npm može da se izvrši u command promptu sa naredbama:

node -v

npm -v

Pored instalacije na lokalnoj mašini koja se koristi za razvoj aplikacje, po završetku aplikacije je potrebno prebaciti aplikaciju na neki hosting koji ima podršku za node.js. Ovaj postupak zavisi od same konfiguracije hosting-a i opcionog sofrvera koji je instaliran na njemu (C-panel, Plesk…). Ukoliko imate na hosting mašini instaliran Plesk ovde možete pročitati kako se obezbedjuje rad node.js na takvom serveru.