Node.js
Node.js

Follow

Feb 24, 2017 – 14 min read

Denna artikel kommer från Tomislav Capan, teknisk konsult och Node.js entusiast. Tomislav publicerade ursprungligen den här artikeln i augusti 2013 i Toptal-bloggen – du hittar det ursprungliga inlägget här; bloggen har uppdaterats något. Följande ämne är baserat på den här författarens åsikter och erfarenheter.

JavaScripts ökande popularitet har fört med sig en hel del förändringar, och ansiktet på webbutveckling idag är dramatiskt annorlunda. De saker som vi kan göra på webben numera med JavaScript som körs på servern, såväl som i webbläsaren, var svåra att föreställa sig för bara några år sedan, eller så var de inkapslade i sandlåda-miljöer som Flash eller Java Applets.

Innan du gräver dig in i Node.js kan du kanske läsa om fördelarna med att använda JavaScript i hela stacken, vilket förenhetligar språket och dataformatet (JSON), vilket gör att du kan återanvända utvecklarresurser på ett optimalt sätt. Eftersom detta är mer en fördel med JavaScript än Node.js specifikt kommer vi inte att diskutera det särskilt mycket här. Men det är en viktig fördel med att införliva Node.js i din stack.

Node.js är en JavaScript-körtidsmiljö som bygger på Chromes V8 JavaScript-motor. Det är värt att notera att Ryan Dahl, skaparen av Node.js, hade som mål att skapa webbplatser i realtid med push-funktion, ”inspirerad av applikationer som Gmail”. I Node.js gav han utvecklare ett verktyg för att arbeta i det icke-blockerande, händelsestyrda I/O-paradigmet.

I en mening: Node.js är en bra lösning för webbtillämpningar i realtid som använder push-teknik via websockets. Vad är det som är så revolutionerande med det? Jo, efter över 20 år av stateless-web baserat på det stateless request-response-paradigmet har vi äntligen webbapplikationer med tvåvägsförbindelser i realtid, där både klienten och servern kan initiera kommunikationen, vilket gör det möjligt för dem att fritt utbyta data.

Detta står i skarp kontrast till det typiska webbrespons-paradigmet, där klienten alltid initierar kommunikationen. Dessutom bygger allt på den öppna webbstacken (HTML, CSS och JS) som körs över standardporten 80.

Man kan hävda att vi har haft detta i åratal i form av Flash och Java Applets – men i själva verket var dessa bara sandlåda-miljöer som använde webben som ett transportprotokoll som levererades till klienten. Dessutom kördes de i isolering och fungerade ofta över icke-standardiserade portar, vilket kan ha krävt extra behörigheter och liknande.

Med alla sina fördelar spelar Node.js nu en viktig roll i teknikstacken hos många högprofilerade företag som är beroende av dess unika fördelar. Node.js Foundation har sammanställt alla de bästa tankegångarna kring varför företag bör överväga Node.js i en kort presentation som kan hittas på Node.js Foundations sida Case Studies.

I det här inlägget kommer jag att diskutera inte bara hur dessa fördelar uppnås, utan också varför du kanske vill använda Node.js – och varför inte – genom att använda några av de klassiska webbapplikationsmodellerna som exempel.

Hur fungerar det?

Den huvudsakliga idén med Node.js: att använda icke-blockerande, händelsestyrd I/O för att förbli lättviktig och effektiv inför dataintensiva realtidstillämpningar som körs på distribuerade enheter.

Det är en ordentlig munsbit.

Vad det egentligen betyder är att Node.js inte är någon ny plattform med en silverbullet som kommer att dominera webbutvecklingsvärlden. I stället är det en plattform som fyller ett särskilt behov. Och det är absolut nödvändigt att förstå detta. Du vill definitivt inte använda Node.js för CPU-intensiva operationer; faktum är att om du använder den för tunga beräkningar kommer du att upphäva nästan alla dess fördelar. Där Node.js verkligen briljerar är när det gäller att bygga snabba, skalbara nätverksapplikationer, eftersom den kan hantera ett stort antal samtidiga anslutningar med hög genomströmning, vilket motsvarar hög skalbarhet.

Hur den fungerar under huven är ganska intressant. Jämfört med traditionella webbserveringstekniker där varje anslutning (begäran) startar en ny tråd, som tar upp systemets RAM-minne och så småningom når maxgränsen för det tillgängliga RAM-minne, kan Node.js arbetar med en enda tråd och använder icke-blockerande I/O-anrop, vilket gör det möjligt att stödja tiotusentals samtidiga anslutningar (som hålls i händelseslingan).

*Bilden är hämtad från det ursprungliga blogginlägget.

En snabb beräkning: Om vi antar att varje tråd potentiellt har 2 MB minne med sig, och körs på ett system med 8 GB RAM, får vi ett teoretiskt maximum på 4 000 samtidiga anslutningar (beräkningarna är hämtade från Michael Abernethys artikel ”Just what is Node.js?”, som publicerades på IBM developerWorks 2011; tyvärr finns artikeln inte längre tillgänglig), plus kostnaden för att växla kontext mellan trådar. Detta är det scenario som du vanligtvis har att göra med i traditionella webbserveringstekniker. Genom att undvika allt detta uppnår Node.js skalbarhetsnivåer på över 1 miljon samtidiga anslutningar och över 600 000 samtidiga websockets-anslutningar.

Det finns förstås frågan om att dela en enda tråd mellan alla klienters förfrågningar, och det är en potentiell fallgrop när man skriver Node.js-program. För det första kan tunga beräkningar kväva Nodes enda tråd och orsaka problem för alla klienter (mer om detta senare) eftersom inkommande förfrågningar blockeras tills beräkningen är klar. För det andra måste utvecklare vara mycket försiktiga så att de inte låter ett undantag bubbla upp till Node.js händelseslinga i kärnan (högst upp), vilket kommer att leda till att Node.js instans avslutas (vilket i praktiken kraschar programmet).

Tekniken som används för att undvika att undantag bubblar upp till ytan är att skicka tillbaka fel till anroparen som callback-parametrar (i stället för att slänga dem, som i andra miljöer). Även om något obehandlat undantag lyckas bubbla upp har verktyg utvecklats för att övervaka Node.js-processen och utföra den nödvändiga återhämtningen av en kraschad instans (även om du förmodligen inte kommer att kunna återställa det aktuella tillståndet för användarsessionen), det vanligaste är Forever-modulen, eller att använda ett annat tillvägagångssätt med de externa systemverktygen upstart och monit, eller till och med bara upstart.

npm: The Node Package Manager

När man diskuterar Node.js är en sak som definitivt inte bör utelämnas det inbyggda stödet för pakethantering med hjälp av verktyget npm som följer med som standard vid varje Node.js installation. Idén med npm-moduler är ganska lik den med Ruby Gems: en uppsättning allmänt tillgängliga, återanvändbara komponenter, tillgängliga genom enkel installation via ett onlineförråd, med versions- och beroendehantering.

En fullständig lista över paketerade moduler finns på npm:s webbplats, eller nås med hjälp av npm CLI-verktyget som automatiskt installeras med Node.js. Modul-ekosystemet är öppet för alla, och vem som helst kan publicera sin egen modul som kommer att listas i npm-repositoriet. En kort introduktion till npm finns i en guide för nybörjare, och detaljer om hur man publicerar moduler finns i npm Publishing Tutorial.

Några av de mest användbara npm-modulerna idag är:

  • express – Express.js, ett Sinatra-inspirerat ramverk för webbutveckling för Node.js, och de-facto standard för de flesta Node.js applikationer som finns idag.
  • hapi – ett mycket modulärt och enkelt konfigurationscentrerat ramverk för att bygga webb- och tjänsteapplikationer
  • connect – Connect är ett utbyggbart HTTP-serverramverk för Node.js, som tillhandahåller en samling högpresterande ”plugins” som kallas middleware; fungerar som en grund för Express.
  • socket.io och sockjs – Server-sidekomponent för de två vanligaste websockets-komponenterna som finns idag.
  • pug (tidigare Jade) – En av de populära mallmotorerna, inspirerad av HAML, standard i Express.js.
  • mongodb och mongojs – MongoDB-wrappers för att tillhandahålla API:et för MongoDB-objektsdatabaser i Node.js.
  • redis – Redis-klientbibliotek.
  • lodash (underscore, lazy.js) – JavaScript-verktygsbälte. Underscore inledde spelet, men blev överkörd av någon av sina två motsvarigheter, främst på grund av bättre prestanda och modulär implementering.
  • forever – Förmodligen det vanligaste verktyget för att se till att ett visst node-skript körs kontinuerligt. Håller din Node.js-process uppe i produktionen vid oväntade fel.
  • bluebird – En fullfjädrad Promises/A+-implementering med exceptionellt bra prestanda
  • moment – Ett lättviktigt JavaScript-datumbibliotek för att analysera, validera, manipulera och formatera datum.

Listan kan fortsätta. Det finns massor av riktigt användbara paket där ute, tillgängliga för alla (inget ont om de som jag har utelämnat här).

Var Node.js bör användas

Chat är den mest typiska realtidstillämpningen för flera användare. Från IRC (på den tiden), via många proprietära och öppna protokoll som körs på icke-standardiserade portar, till möjligheten att implementera allting idag i Node.js med websockets som körs över standardporten 80.

Chatttillämpningen är verkligen det bästa exemplet för Node.js: det är en lättviktig, hög trafik, dataintensiv (men med låg bearbetnings-/beräkningskapacitet) tillämpning som körs över distribuerade enheter. Det är också ett utmärkt användningsfall för inlärning också, eftersom det är enkelt, men ändå täcker de flesta paradigm du någonsin kommer att använda i en typisk Node.js applikation.

Låt oss försöka skildra hur det fungerar.

I det enklaste scenariot har vi ett enda chattrum på vår webbplats dit folk kommer och kan utbyta meddelanden på ett en-till-många (egentligen alla) sätt. Säg till exempel att vi har tre personer på webbplatsen som alla är anslutna till vår anslagstavla.

På serversidan har vi en enkel Express.js-applikation som implementerar två saker: 1) en GET ’/’-förfrågningshanterare som serverar webbsidan som innehåller både en anslagstavla och en ’Send’-knapp för att initiera inmatning av nya meddelanden, och 2) en websockets-server som lyssnar på nya meddelanden som sänds ut av websockets-klienter.

På klientsidan har vi en HTML-sida med ett par handlers som är inställda, en för klickhändelsen för ’Send’-knappen, som plockar upp inmatningsmeddelandet och skickar det ner i websocketen, och en annan som lyssnar på nya inkommande meddelanden på websockets-klienten (dvs, meddelanden som skickats av andra användare och som servern nu vill att klienten ska visa).

När en av klienterna skickar ett meddelande händer följande:

  • Navigatorn fångar upp klicket på knappen ”Skicka” med hjälp av en JavaScript-hanterare, hämtar värdet från inmatningsfältet (dvs, meddelandetexten), och skickar ett websocket-meddelande med hjälp av den websocket-klient som är ansluten till vår server (initierad vid initialiseringen av webbsidan).
  • Serverkomponenten i websocket-anslutningen tar emot meddelandet och vidarebefordrar det till alla andra anslutna klienter med hjälp av broadcast-metoden.
  • Alla klienter tar emot det nya meddelandet som ett push-meddelande via en websockets-komponent på klientsidan som körs i webbsidan. De plockar sedan upp meddelandets innehåll och uppdaterar webbsidan på plats genom att lägga till det nya meddelandet på tavlan.

Bilden är hämtad från den ursprungliga bloggen.

Detta är det enklaste exemplet. Om du vill ha en mer robust lösning kan du använda en enkel cache baserad på Redis-lagret. Eller, i en ännu mer avancerad lösning, en meddelandekö för att hantera dirigering av meddelanden till klienter och en mer robust leveransmekanism som kan täcka tillfälliga anslutningsförluster eller lagra meddelanden för registrerade klienter medan de är offline. Men oavsett vilka förbättringar du gör kommer Node.js fortfarande att fungera enligt samma grundprinciper: reagera på händelser, hantera många samtidiga anslutningar och bibehålla en flytande användarupplevelse.

API ON TOP OF AN OBJECT DB

Och även om Node.js verkligen briljerar med realtidsapplikationer är det en ganska naturlig lösning för att exponera data från objektdatabaser (t.ex. MongoDB). JSON-lagrade data gör det möjligt för Node.js att fungera utan impedansmissmatchning och datakonvertering.

Om du till exempel använder Rails skulle du konvertera från JSON till binära modeller och sedan exponera dem tillbaka som JSON via HTTP när datan konsumeras av React.js, Angular.js etc., eller till och med vanliga jQuery AJAX-anrop. Med Node.js kan du helt enkelt exponera dina JSON-objekt med ett REST API så att klienten kan konsumera dem. Dessutom behöver du inte oroa dig för att konvertera mellan JSON och vad som helst annat när du läser eller skriver från din databas (om du använder MongoDB). Sammanfattningsvis kan du undvika behovet av flera konverteringar genom att använda ett enhetligt dataserialiseringsformat i klienten, servern och databasen.

KVALIFICERADE INPUT

Om du tar emot en stor mängd samtidiga data kan din databas bli en flaskhals. Som visas ovan kan Node.js enkelt hantera de samtidiga anslutningarna själv. Men eftersom databasåtkomst är en blockerande operation (i det här fallet) får vi problem. Lösningen är att bekräfta klientens beteende innan data verkligen skrivs till databasen.

Med det tillvägagångssättet behåller systemet sin reaktionsförmåga under hög belastning, vilket är särskilt användbart när klienten inte behöver en säker bekräftelse på en lyckad dataskrivning. Typiska exempel är loggning eller skrivning av data för användarkontroll, som behandlas i omgångar och inte används förrän vid en senare tidpunkt, samt operationer som inte behöver återspeglas omedelbart (t.ex. uppdatering av antalet ”Likes” på Facebook) där eventuell konsistens (som så ofta används i NoSQL-världen) är godtagbar.

Data läggs i kö via någon form av infrastruktur för cachelagring eller Message Queuing (MQ) (t.ex, RabbitMQ, ZeroMQ) och bearbetas av en separat batch-skrivningsprocess i en separat databas, eller av backend-tjänster för beräkningsintensiv bearbetning, som är skrivna i en plattform med bättre prestanda för sådana uppgifter. Liknande beteende kan implementeras med andra språk/ramverk, men inte på samma hårdvara, med samma höga, bibehållna genomströmning.

Bild hämtad från originalartikeln.

Kort sagt: med Node kan du skjuta databasskrivningarna åt sidan och hantera dem senare och fortsätta som om de lyckades.

DATA STREAMING

I mer traditionella webbplattformar behandlas HTTP-begäranden och -svar som isolerade händelser; i själva verket är de faktiskt strömmar. Denna observation kan utnyttjas i Node.js för att bygga några coola funktioner. Det är till exempel möjligt att bearbeta filer medan de fortfarande laddas upp, eftersom data kommer in via en ström och vi kan bearbeta dem online. Detta kan göras för ljud- eller videokodning i realtid och proxykodning mellan olika datakällor (se nästa avsnitt).

PROXY

Node.js kan lätt användas som en proxy på serversidan där den kan hantera ett stort antal samtidiga anslutningar på ett icke-blockerande sätt. Den är särskilt användbar för att proxya olika tjänster med olika svarstider eller för att samla in data från flera källpunkter.

Ett exempel: tänk på ett program på serversidan som kommunicerar med resurser från tredje part, hämtar data från olika källor eller lagrar tillgångar som bilder och videoklipp till molntjänster från tredje part.

Och även om det finns dedikerade proxyservrar, kan det vara användbart att använda Node i stället om din proxy-infrastruktur är obefintlig eller om du behöver en lösning för lokal utveckling. Med detta menar jag att du kan bygga en app på klientsidan med en Node.js-utvecklingsserver för tillgångar och proxysökning/stubbning av API-förfrågningar, medan du i produktionen hanterar sådana interaktioner med en dedikerad proxytjänst (nginx, HAProxy etc.).

BROKERAGE – STOCK TRADER’S DASHBOARD

Låt oss återgå till applikationsnivån. Ett annat exempel där skrivbordsprogram dominerar, men som lätt skulle kunna ersättas med en webbaserad lösning i realtid, är mäklares handelsprogram, som används för att följa aktiekurserna, utföra beräkningar/tekniska analyser och skapa grafer/diagram.

Omställningen till en webbaserad lösning i realtid skulle göra det möjligt för mäklarna att enkelt byta arbetsstation eller arbetsplats. Snart kanske vi börjar se dem på stranden i Florida, Ibiza eller Bali.

APPLIKATIONSMONITORING DASHBOARD

Ett annat vanligt användningsfall där Node-with-web-sockets passar perfekt: Spårning av webbplatsbesökare och visualisering av deras interaktioner i realtid. Du kan samla in statistik i realtid från din användare, eller till och med ta det till nästa nivå genom att införa riktade interaktioner med dina besökare genom att öppna en kommunikationskanal när de når en viss punkt i din tratt – ett exempel på detta finns med CANDDi.

Föreställ dig hur du skulle kunna förbättra din verksamhet om du visste vad dina besökare gjorde i realtid – om du kunde visualisera deras interaktioner. Med de tvåvägssocklar i realtid som finns i Node.js kan du nu göra det.

SYSTEMÖVERVAKNINGS-DASHBOARD

Nu ska vi titta på infrastruktursidan av saker och ting. Tänk dig till exempel en SaaS-leverantör som vill erbjuda sina användare en sida för tjänsteövervakning (till exempel GitHub Status-sidan). Med Node.js händelseslinga kan vi skapa en kraftfull webbaserad instrumentpanel som kontrollerar tjänsternas status på ett asynkront sätt och skickar data till klienterna med hjälp av websockets.

Med hjälp av den här tekniken kan både interna (inom företaget) och offentliga tjänsters status rapporteras live och i realtid. Fördjupa den tanken lite ytterligare och försök att föreställa dig ett Network Operations Center (NOC) som övervakar tillämpningar hos en teleoperatör, en moln-/nätverks-/hostingleverantör eller ett finansinstitut, som alla körs på den öppna webbstacken med stöd av Node.js och websockets i stället för Java och/eller Java Applets.

Notera: Försök inte att bygga hårda realtidssystem i Node.js (dvs. system som kräver konsekventa svarstider). Erlang är förmodligen ett bättre val för den typen av tillämpningar.

SERVER-SIDE WEB APPLICATIONS

Node.js med Express.js kan också användas för att skapa klassiska webbapplikationer på serversidan. Även om det är möjligt är detta paradigm med begäran-svar, där Node.js transporterar renderad HTML, inte det mest typiska användningsfallet. Det finns argument för och emot detta tillvägagångssätt. Här är några fakta att ta hänsyn till:

Pros:

  • Om din applikation inte har några CPU-intensiva beräkningar kan du bygga den i Javascript uppifrån och ner, till och med ner till databasnivån om du använder JSON storage Object DB som MongoDB. Detta underlättar utvecklingen (inklusive anställning) avsevärt.
  • Crawlers får ett helt renderat HTML-svar, vilket är mycket mer SEO-vänligt än t.ex. en Single Page Application eller en websockets-app som körs ovanpå Node.js.

Konsekvenser:

  • Alla CPU-intensiva beräkningar blockerar Node.js responsförmåga, så en plattform med trådar är en bättre strategi. Alternativt kan du försöka skala ut beräkningen(*).
  • Användning av Node.js med en relationsdatabas är fortfarande ganska besvärligt (se nedan för mer information). Gör dig själv en tjänst och välj någon annan miljö som Rails, Django eller ASP.Net MVC om du försöker utföra relationella operationer.

(*) Ett alternativ till CPU-intensiva beräkningar är att skapa en mycket skalbar MQ-backed-miljö med backend-bearbetning för att behålla Node som en ”kontorist” på framsidan som hanterar klientförfrågningar asynkront.

SERVER-SIDE WEB APPLICATION WITH A RELATIONAL DATABASE BEHIND

Vid jämförelse av Node.js med Express.js mot Ruby on Rails, till exempel, finns det ett klart beslut till förmån för det senare när det gäller tillgång till relationella data.

Relationella DB-verktyg för Node.js är fortfarande ganska underutvecklade, jämfört med konkurrenterna. Å andra sidan tillhandahåller Rails automatiskt dataåtkomstinstallation direkt ur lådan tillsammans med stödverktyg för DB-schemamigreringar och andra pärlor (ordvitsar). Rails och dess jämbördiga ramverk har mogna och beprövade Active Record- eller Data Mapper-implementationer av dataåtkomstskiktet, som du kommer att sakna om du försöker replikera dem i ren JavaScript.(*)

Och om du verkligen är benägen att fortsätta med JS hela vägen kan du kolla in Sequelize och Node ORM2.

(*) Det är möjligt och inte ovanligt att använda Node.js enbart som en offentlig fasad, samtidigt som du behåller din Rails-backend och dess enkla tillgång till en relationsdatabas.

HEAVY SERVER-SIDE COMPUTATION/PROCESSING

När det gäller tunga beräkningar är Node.js inte den bästa plattformen som finns. Nej, du vill definitivt inte bygga en server för Fibonacci-beräkning i Node.js. I allmänhet upphäver alla CPU-intensiva operationer alla de genomströmningsfördelar som Node erbjuder med sin händelsestyrda, icke-blockerande I/O-modell eftersom alla inkommande förfrågningar kommer att blockeras medan tråden är upptagen med din sifferberäkning.

Som tidigare nämnts är Node.js enkeltrådad och använder endast en enda CPU-kärna. När det gäller att lägga till samtidighet på en server med flera kärnor finns det en del arbete som utförs av Node core-teamet i form av en klustermodul. Du kan också köra flera Node.js-serverinstanser ganska enkelt bakom en omvänd proxy via nginx.

Med kluster bör du fortfarande avlasta alla tunga beräkningar till bakgrundsprocesser som är skrivna i en mer lämplig miljö för detta, och låta dem kommunicera via en server för meddelandeköer som RabbitMQ.

Även om bakgrundsbearbetningen kanske körs på samma server inledningsvis, så har ett sådant tillvägagångssätt potential för mycket hög skalbarhet. Dessa bakgrundsbehandlingstjänster kan enkelt distribueras ut till separata arbetsservrar utan att behöva konfigurera belastningen på de webbservrar som är vända mot fronten.

Självklart kan du använda samma tillvägagångssätt även på andra plattformar, men med Node.js får du den höga reqs/sec-genomströmning som vi har talat om, eftersom varje begäran är en liten uppgift som hanteras mycket snabbt och effektivt.

Slutsats

Vi har diskuterat Node.js från teori till praktik, med början med dess mål och ambitioner och slutar med dess smultronställen och fallgropar. När folk stöter på problem med Node kokar det nästan alltid ner till det faktum att blockerande operationer är roten till allt ont – 99 % av Node-missbruken kommer som en direkt följd av detta.

Håll dig i minnet: Node.js skapades aldrig för att lösa problemet med skalning av beräkningskapacitet. Det skapades för att lösa problemet med I/O-skalering, vilket det gör riktigt bra.