Node.js
Node.js

Follow

Feb 24, 2017 – 14 min read

Denne artikel kommer fra Tomislav Capan, teknisk konsulent og Node.js entusiast. Tomislav offentliggjorde oprindeligt denne artikel i august 2013 på Toptal-bloggen – du kan finde det oprindelige indlæg her; bloggen er blevet en smule opdateret. Det følgende emne er baseret på denne forfatters mening og erfaringer.

JavaScript’s stigende popularitet har medført en masse ændringer, og ansigtet af webudvikling i dag er dramatisk anderledes. De ting, vi kan gøre på nettet i dag med JavaScript, der kører på serveren såvel som i browseren, var svært at forestille sig for blot nogle år siden, eller var indkapslet i sandboxede miljøer som Flash eller Java-applets.

Hvor du kaster dig ud i Node.js, kan du måske læse om fordelene ved at bruge JavaScript på tværs af stakken, som ensretter sproget og dataformatet (JSON), så du kan genbruge udviklerressourcerne optimalt. Da dette mere er en fordel ved JavaScript end Node.js specifikt, vil vi ikke diskutere det meget her. Men det er en vigtig fordel ved at inkorporere Node.js i din stack.

Node.js er et JavaScript-køringstidsmiljø, der er bygget på Chromes V8 JavaScript-motor. Det er værd at bemærke, at Ryan Dahl, skaberen af Node.js, havde til formål at skabe realtidswebsteder med push-mulighed, “inspireret af applikationer som Gmail”. I Node.js gav han udviklere et værktøj til at arbejde i det ikke-blokkerende, begivenhedsdrevne I/O-paradigme.

I én sætning: Node.js skinner i realtidswebapplikationer, der anvender push-teknologi over websockets. Hvad er der så revolutionerende ved det? Jo, efter over 20 år med stateless-web baseret på det stateless request-response-paradigme har vi endelig webapplikationer med tovejsforbindelser i realtid, hvor både klienten og serveren kan indlede kommunikationen, så de frit kan udveksle data.

Dette står i skarp kontrast til det typiske webresponse-paradigme, hvor klienten altid indleder kommunikationen. Desuden er det hele baseret på den åbne webstack (HTML, CSS og JS), der kører over standardport 80.

Man kan hævde, at vi har haft dette i årevis i form af Flash- og Java-applets – men i virkeligheden var det blot sandkassemiljøer, der brugte nettet som en transportprotokol, der blev leveret til klienten. Desuden blev de kørt isoleret og fungerede ofte over ikke-standardiserede porte, hvilket kan have krævet ekstra tilladelser og lignende.

Med alle sine fordele spiller Node.js nu en afgørende rolle i teknologistakken hos mange højt profilerede virksomheder, der er afhængige af dets unikke fordele. Node.js Foundation har samlet alle de bedste tanker omkring, hvorfor virksomheder bør overveje Node.js i en kort præsentation, der kan findes på Node.js Foundations side om casestudier.

I dette indlæg vil jeg ikke kun diskutere, hvordan disse fordele opnås, men også hvorfor du måske ønsker at bruge Node.js – og hvorfor ikke – ved hjælp af nogle af de klassiske webapplikationsmodeller som eksempler.

Hvordan virker det?

Den vigtigste idé med Node.js: Brug ikke-blokkerende, begivenhedsdrevet I/O for at forblive let og effektiv i forhold til dataintensive realtidsapplikationer, der kører på tværs af distribuerede enheder.

Det er en mundfuld.

Hvad det egentlig betyder er, at Node.js ikke er en ny sølvkugle-platform, der vil dominere webudviklingsverdenen. I stedet er det en platform, der udfylder et særligt behov. Og det er helt afgørende at forstå dette. Du ønsker bestemt ikke at bruge Node.js til CPU-intensive operationer; faktisk vil brugen af den til tunge beregninger annullere næsten alle dens fordele. Der, hvor Node.js virkelig brillerer, er ved opbygning af hurtige, skalerbare netværksapplikationer, da den er i stand til at håndtere et stort antal samtidige forbindelser med høj gennemstrømning, hvilket er lig med høj skalerbarhed.

Hvordan den fungerer under motorhjelmen er ret interessant. Sammenlignet med traditionelle web-serviceteknikker, hvor hver forbindelse (forespørgsel) starter en ny tråd, der optager systemets RAM og i sidste ende når sit maksimum ved den mængde RAM, der er til rådighed, er Node.js opererer på en enkelt tråd ved hjælp af ikke-blockerende I/O-kald, hvilket gør det muligt at understøtte titusindvis af samtidige forbindelser (som holdes i event loop).

*Billedet er taget fra det oprindelige blogindlæg.

En hurtig beregning: Hvis vi antager, at hver tråd potentielt har 2 MB hukommelse med sig. Hvis vi kører på et system med 8 GB RAM, kommer vi op på et teoretisk maksimum på 4.000 samtidige forbindelser (beregninger taget fra Michael Abernethys artikel “Just what is Node.js?”, der blev offentliggjort på IBM developerWorks i 2011; artiklen er desværre ikke længere tilgængelig), plus omkostningerne ved kontekstskift mellem tråde. Det er det scenarie, du typisk har med at gøre i traditionelle web-serviceteknikker. Ved at undgå alt dette opnår Node.js skalerbarhedsniveauer på over 1 mio. samtidige forbindelser og over 600k samtidige websockets-forbindelser.

Der er selvfølgelig spørgsmålet om at dele en enkelt tråd mellem alle klienters anmodninger, og det er en potentiel faldgrube ved at skrive Node.js-applikationer. For det første kan tunge beregninger kvæle Node’s enkelte tråd og skabe problemer for alle klienter (mere om dette senere), da indkommende anmodninger vil blive blokeret, indtil den pågældende beregning er afsluttet. For det andet skal udviklere være meget forsigtige med ikke at lade en undtagelse boble op til den centrale (øverste) Node.js-hændelsessløjfe, hvilket vil medføre, at Node.js-instansen afsluttes (hvilket effektivt vil få programmet til at gå ned).

Den teknik, der bruges til at undgå, at undtagelser bobler op til overfladen, er at sende fejl tilbage til callback-parametrene (i stedet for at smide dem, som i andre miljøer). Selv hvis en ubehandlet undtagelse formår at boble op, er der udviklet værktøjer til at overvåge Node.js-processen og udføre den nødvendige genopretning af en nedstyrtet instans (selv om du sandsynligvis ikke vil være i stand til at genoprette den aktuelle tilstand af brugersessionen), det mest almindelige er Forever-modulet, eller ved at bruge en anden tilgang med eksterne systemværktøjer upstart og monit, eller endda bare upstart.

npm: The Node Package Manager

Når man diskuterer Node.js, er der en ting, der bestemt ikke bør udelades, nemlig den indbyggede understøttelse af pakkehåndtering ved hjælp af værktøjet npm, der som standard følger med enhver Node.js-installation. Ideen med npm-moduler minder meget om den, der ligger bag Ruby Gems: et sæt offentligt tilgængelige, genanvendelige komponenter, der er tilgængelige gennem nem installation via et online-repositorium, med versions- og afhængighedsstyring.

En komplet liste over pakkede moduler kan findes på npm-webstedet eller fås ved hjælp af npm CLI-værktøjet, der automatisk installeres med Node.js. Moduløkosystemet er åbent for alle, og alle kan udgive deres eget modul, som vil blive opført i npm-repositoriet. Du kan finde en kort introduktion til npm i en begynderguide og detaljer om udgivelse af moduler i npm Publishing Tutorial.

Nogle af de mest nyttige npm-moduler i dag er:

  • express – Express.js, en Sinatra-inspireret webudviklingsramme til Node.js, og de-facto standarden for de fleste Node.js applikationer der findes i dag.
  • hapi – en meget modulær og enkel at bruge konfigurationscentreret ramme til opbygning af web- og tjenesteapplikationer
  • connect – Connect er en udvidelig HTTP-serverramme til Node.js, der leverer en samling af højtydende “plugins”, der er kendt som middleware; tjener som et basisfundament for Express.
  • socket.io og sockjs – Server-side komponent af de to mest almindelige websockets-komponenter, der findes i dag.
  • pug (tidligere Jade) – En af de populære templating engines, inspireret af HAML, en standard i Express.js.
  • mongodb og mongojs – MongoDB-wrappere til at levere API’en for MongoDB-objektdatabaser i Node.js.
  • redis – Redis-klientbibliotek.
  • lodash (underscore, lazy.js) – JavaScript-værktøjsbælte. Underscore indledte spillet, men blev overhalet af en af sine to modstykker, primært på grund af bedre ydeevne og modulær implementering.
  • forever – Sandsynligvis det mest almindelige hjælpeprogram til at sikre, at et givent node-script kører kontinuerligt. Holder din Node.js-proces oppe i produktionen i tilfælde af uventede fejl.
  • bluebird – En fuldt udstyret Promises/A+-implementering med usædvanlig god ydeevne
  • moment – Et letvægts-JavaScript-datobibliotek til parsing, validering, manipulering og formatering af datoer.

Listen fortsætter. Der er tonsvis af virkelig nyttige pakker derude, som er tilgængelige for alle (ingen fornærmelse mod dem, som jeg har udeladt her).

Hvor Node.js bør bruges

Chat er den mest typiske realtids- og flerbrugerapplikation. Fra IRC (tilbage i tiden), gennem mange proprietære og åbne protokoller, der kører på ikke-standardiserede porte, til muligheden for at implementere alt i dag i Node.js med websockets, der kører over standardporten 80.

Chat-applikationen er virkelig det sødeste eksempel for Node.js: det er en letvægtsapplikation med høj trafik, dataintensiv (men lav behandling/beregning), der kører på tværs af distribuerede enheder. Det er også en god use-case for læring også, da det er simpelt, men det dækker de fleste af de paradigmer, du nogensinde vil bruge i en typisk Node.js applikation.

Lad os prøve at skildre, hvordan det virker.

I det enkleste scenarie har vi et enkelt chatrum på vores hjemmeside, hvor folk kommer og kan udveksle beskeder på en-til-mange (faktisk alle) måde. Lad os f.eks. sige, at vi har tre personer på webstedet, der alle er forbundet til vores opslagstavle.

På serversiden har vi en simpel Express.js-applikation, som implementerer to ting: 1) en GET ‘/’-forespørgselshåndtering, som betjener websiden, der indeholder både en opslagstavle og en ‘Send’-knap til at initialisere indtastning af nye meddelelser, og 2) en websockets-server, der lytter efter nye meddelelser, der udsendes af websocket-klienter.

På klientsiden har vi en HTML-side med et par handlere oprettet, en for ‘Send’-knapklikhændelsen, som opfanger indtastningsmeddelelsen og sender den ned i websocket’en, og en anden, der lytter efter nye indgående meddelelser på websocket-klienten (dvs, meddelelser sendt af andre brugere, som serveren nu ønsker, at klienten skal vise).

Når en af klienterne sender en meddelelse, sker der følgende:

  • Browser fanger “Send”-knapklikket via en JavaScript-handler, opsamler værdien fra indtastningsfeltet (dvs, beskedteksten), og udsender en websocket-meddelelse ved hjælp af den websocket-klient, der er tilsluttet vores server (initialiseret ved initialiseringen af websiden).
  • Serverkomponenten på websocket-forbindelsen modtager meddelelsen og videresender den til alle andre tilsluttede klienter ved hjælp af broadcast-metoden.
  • Alle klienter modtager den nye meddelelse som en push-meddelelse via en websockets-komponent på klientsiden, der kører i websiden. De henter derefter beskedindholdet og opdaterer websiden på stedet ved at tilføje den nye besked til tavlen.

Billedet er taget fra den oprindelige blog.

Dette er det enkleste eksempel. Hvis du ønsker en mere robust løsning, kan du bruge en simpel cache baseret på Redis-lageret. Eller i en endnu mere avanceret løsning en meddelelseskø til at håndtere videresendelse af meddelelser til klienter og en mere robust leveringsmekanisme, der kan dække midlertidige tab af forbindelse eller opbevaring af meddelelser til registrerede klienter, mens de er offline. Men uanset hvilke forbedringer du foretager, vil Node.js stadig fungere efter de samme grundlæggende principper: at reagere på begivenheder, håndtere mange samtidige forbindelser og opretholde en flydende brugeroplevelse.

API OVEN PÅ EN OBJECT DB

Men selv om Node.js virkelig skinner med realtidsapplikationer, passer det ganske naturligt til eksponering af data fra objekt DB’er (f.eks. MongoDB). JSON-lagrede data gør det muligt for Node.js at fungere uden impedansforskydning og datakonvertering.

For eksempel, hvis du bruger Rails, vil du konvertere fra JSON til binære modeller og derefter eksponere dem tilbage som JSON over HTTP, når dataene forbruges af React.js, Angular.js osv. eller endda almindelige jQuery AJAX-opkald. Med Node.js kan du blot udsætte dine JSON-objekter med et REST API, som klienten kan forbruge. Derudover behøver du ikke at bekymre dig om at konvertere mellem JSON og hvad som helst andet, når du læser eller skriver fra din database (hvis du bruger MongoDB). Alt i alt kan du undgå behovet for flere konverteringer ved at bruge et ensartet dataserialiseringsformat på tværs af klient, server og database.

KVALIFICEREDE INPUTS

Hvis du modtager en stor mængde samtidige data, kan din database blive en flaskehals. Som afbildet ovenfor kan Node.js nemt selv håndtere de samtidige forbindelser. Men fordi databaseadgang er en blokerende operation (i dette tilfælde), løber vi ind i problemer. Løsningen er at anerkende klientens adfærd, før dataene virkelig er skrevet til databasen.

Med denne fremgangsmåde bevarer systemet sin reaktionsevne under stor belastning, hvilket er særlig nyttigt, når klienten ikke har brug for en sikker bekræftelse af en vellykket dataskrivning. Typiske eksempler omfatter: logning eller skrivning af data til brugersporing, der behandles i batches og først bruges på et senere tidspunkt; samt operationer, der ikke behøver at blive afspejlet øjeblikkeligt (som f.eks. opdatering af et “Synes godt om”-tal på Facebook), hvor eventuel konsistens (der så ofte anvendes i NoSQL-verdenen) er acceptabel.

Data bliver sat i kø via en form for caching- eller message queuing (MQ)-infrastruktur (f.eks, RabbitMQ, ZeroMQ) og fordøjes af en separat database batch-write-proces eller beregningsintensive behandlingsbackend-tjenester, der er skrevet i en platform med bedre ydeevne til sådanne opgaver. Lignende adfærd kan gennemføres med andre sprog/rammer, men ikke på samme hardware, med samme høje, opretholdte gennemløb.

Billede taget fra originalartikel.

Kort sagt: Med Node kan du skubbe databaseskriverne ud til siden og håndtere dem senere og fortsætte, som om de lykkedes.

DATA STREAMING

I mere traditionelle webplatforme behandles HTTP-forespørgsler og svar som isolerede hændelser; faktisk er de faktisk streams. Denne observation kan udnyttes i Node.js til at bygge nogle smarte funktioner. Det er f.eks. muligt at behandle filer, mens de stadig bliver uploadet, da dataene kommer ind via en stream, og vi kan behandle dem online. Dette kunne gøres til realtidskodning af lyd eller video og proxying mellem forskellige datakilder (se næste afsnit).

PROXY

Node.js kan nemt anvendes som en server-side proxy, hvor den kan håndtere et stort antal samtidige forbindelser på en ikke-blockende måde. Det er især nyttigt til proxyservering af forskellige tjenester med forskellige svartider eller indsamling af data fra flere kildepunkter.

Et eksempel: Overvej et program på serversiden, der kommunikerer med tredjepartsressourcer, henter data fra forskellige kilder eller lagrer aktiver som billeder og videoer til tredjeparts cloud-tjenester.

Selv om der findes dedikerede proxyservere, kan det være nyttigt at bruge Node i stedet, hvis din proxy-infrastruktur ikke findes, eller hvis du har brug for en løsning til lokal udvikling. Med dette mener jeg, at du kan bygge en klientsideapp med en Node.js-udviklingsserver til aktiver og proxying/stubbing af API-forespørgsler, mens du i produktionen håndterer sådanne interaktioner med en dedikeret proxytjeneste (nginx, HAProxy osv.).

BROKERAGE – STOCK TRADER’S DASHBOARD

Lad os komme tilbage til applikationsniveauet. Et andet eksempel, hvor desktop-software dominerer, men som nemt kunne erstattes af en webløsning i realtid, er mæglernes handelssoftware, der bruges til at følge aktiekurserne, foretage beregninger/tekniske analyser og oprette grafer/diagrammer.

Skift til en webbaseret løsning i realtid ville give mæglerne mulighed for nemt at skifte arbejdsstation eller arbejdsplads. Snart vil vi måske begynde at se dem på stranden i Florida… eller Ibiza… eller Bali.

APPLIKATIONSMONITORING DASHBOARD

En anden almindelig brugssituation, hvor Node-with-web-sockets passer perfekt: sporing af besøgende på websites og visualisering af deres interaktioner i realtid. Du kan indsamle statistik i realtid fra din bruger eller endda flytte det til det næste niveau ved at indføre målrettede interaktioner med dine besøgende ved at åbne en kommunikationskanal, når de når et bestemt punkt i din tragt – et eksempel på dette kan findes med CANDDi.

Tænk på, hvordan du kunne forbedre din forretning, hvis du vidste, hvad dine besøgende gjorde i realtid – hvis du kunne visualisere deres interaktioner. Med Node.js’ tovejssoketter i realtid kan du nu gøre det.

SYSTEM OVERVÅGNINGS DASHBOARD

Nu skal vi se på infrastruktursiden af tingene. Forestil dig f.eks. en SaaS-udbyder, der ønsker at tilbyde sine brugere en side til serviceovervågning (f.eks. GitHub-statussiden). Med Node.js-hændelsessløjfen kan vi oprette et kraftfuldt webbaseret dashboard, der kontrollerer tjenesternes status på en asynkron måde og skubber data til klienterne ved hjælp af websockets.

Både interne (inden for virksomheden) og offentlige tjenesters status kan rapporteres live og i realtid ved hjælp af denne teknologi. Skub den idé lidt videre og prøv at forestille dig et Network Operations Center (NOC), der overvåger applikationer i et teleselskab, en cloud-/netværks-/hostingudbyder eller en eller anden finansiel institution, som alle kører på den åbne webstack understøttet af Node.js og websockets i stedet for Java og/eller Java-applets.

Bemærk: Forsøg ikke at bygge hårde realtidssystemer i Node.js (dvs. systemer, der kræver konsistente svartider). Erlang er sandsynligvis et bedre valg til den klasse af applikationer.

SERVER-SIDE WEBANVENDELSER

Node.js med Express.js kan også bruges til at oprette klassiske webapplikationer på serversiden. Selv om det er muligt, er dette request-response-paradigme, hvor Node.js ville bære rundt på renderet HTML, dog ikke det mest typiske anvendelsestilfælde, selv om det er muligt. Der er argumenter for og imod denne fremgangsmåde. Her er nogle fakta at overveje:

Pros:

  • Hvis din applikation ikke har nogen CPU-intensive beregninger, kan du bygge den i Javascript fra top til bund, selv ned til databaseniveauet, hvis du bruger JSON-opbevaring Object DB som MongoDB. Dette letter udviklingen (herunder ansættelse) betydeligt.
  • Crawlere modtager et fuldt renderet HTML-svar, hvilket er langt mere SEO-venligt end f.eks. en Single Page Application eller en websockets-app, der køres oven på Node.js.

Cons:

  • Alle CPU-intensive beregninger vil blokere Node.js-reaktionsdygtighed, så en platform med tråde er en bedre tilgang. Alternativt kan du prøve at skalere beregningen ud(*).
  • Brug af Node.js med en relationel database er stadig ret besværligt (se nedenfor for flere detaljer). Gør dig selv en tjeneste og vælg et andet miljø som Rails, Django eller ASP.Net MVC, hvis du forsøger at udføre relationelle operationer.

(*) Et alternativ til CPU-intensive beregninger er at oprette et meget skalerbart MQ-backed miljø med back-end-behandling for at beholde Node som en “ekspedient” på forsiden, der håndterer klientforespørgsler asynkront.

SERVER-SIDE WEB-ANVENDELSE MED EN RELATIONEL DATABASE BAGGRUND

Hvis man sammenligner Node.js med Express.js mod f.eks. Ruby on Rails, er der en klar beslutning til fordel for sidstnævnte, når det drejer sig om relationel dataadgang.

Relationelle DB-værktøjer til Node.js er stadig ret underudviklede, sammenlignet med konkurrenterne. På den anden side giver Rails automatisk dataadgangsopsætning lige ud af boksen sammen med DB-skema-migrationsstøtteværktøjer og andre ædelsten (pun intended). Rails og dets ligestillede frameworks har modne og gennemprøvede Active Record- eller Data Mapper-datatilgangslagerimplementeringer, som du vil savne alvorligt, hvis du forsøger at replikere dem i ren JavaScript.(*)

Såfremt du virkelig er tilbøjelig til at forblive JS hele vejen, kan du alligevel tjekke Sequelize og Node ORM2.

(*) Det er muligt og ikke ualmindeligt at bruge Node.js udelukkende som en offentlig facade, mens du beholder din Rails back-end og dens nemme adgang til en relationel DB.

HEAVY SERVER-SIDE COMPUTATION/PROCESSING

Når det kommer til tunge beregninger, er Node.js ikke den bedste platform, der findes. Nej, du ønsker bestemt ikke at bygge en Fibonacci-beregningsserver i Node.js. Generelt annullerer enhver CPU-intensiv operation alle de gennemløbsfordele, som Node tilbyder med sin hændelsesdrevne, ikke-blockerende I/O-model, fordi alle indkommende forespørgsler vil blive blokeret, mens tråden er optaget af din talberegning.

Som tidligere nævnt er Node.js single-threaded og bruger kun en enkelt CPU-kerne. Når det drejer sig om at tilføje samtidighed på en server med flere kerner, er der noget arbejde i gang hos Node-kerneteamet i form af et clustermodul. Du kan også ret nemt køre flere Node.js-serverinstanser bag en reverse proxy via nginx.

Med clustering bør du stadig aflaste alle tunge beregninger til baggrundsprocesser, der er skrevet i et mere passende miljø til det, og lade dem kommunikere via en beskedkøserver som RabbitMQ.

Selv om din baggrundsbehandling måske køres på den samme server i starten, har en sådan tilgang potentiale for meget høj skalerbarhed. Disse baggrundsbehandlingstjenester kan nemt distribueres ud til separate arbejdsservere uden at skulle konfigurere belastningerne af de frontvendte webservere.

Selvfølgelig kan du også bruge den samme fremgangsmåde på andre platforme, men med Node.js får du det høje reqs/sec-gennemløb, som vi har talt om, da hver forespørgsel er en lille opgave, der håndteres meget hurtigt og effektivt.

Slutning

Vi har diskuteret Node.js fra teori til praksis, begyndende med dens mål og ambitioner og sluttende med dens sweet spots og faldgruber. Når folk løber ind i problemer med Node, koger det næsten altid ned til det faktum, at blokerende operationer er roden til alt ondt – 99 % af Node-misbrug kommer som en direkte konsekvens heraf.

Husk: Node.js blev aldrig skabt for at løse problemet med beregningsskalering. Det blev skabt for at løse I/O-skalerings-problemet, hvilket det gør rigtig godt.