learning node.js for .net developers€¦ · isomorphic javascript writing npm packages defining an...
TRANSCRIPT
TableofContents
LearningNode.jsfor.NETDevelopersCreditsAbouttheAuthorAbouttheReviewerwww.PacktPub.com
eBooks,discountoffers,andmoreWhysubscribe?
PrefaceWhatthisbookcoversWhatyouneedforthisbookWhothisbookisforConventionsReaderfeedbackCustomersupport
DownloadingtheexamplecodeDownloadingthecolorimagesofthisbookErrataPiracyQuestions
1.WhyNode.js?WhatisNode.js?
UnderstandingtheNode.jsexecutionmodelNon-blockingEvent-drivenSingle-threaded
IntroducingtheNode.jsecosystemWhyJavaScript?
AclearcanvasFunctionalnatureAbrightfuture
WhentouseNode.jsWritingwebapplicationsIdentifyingotherusecasesWhynow?
Summary2.GettingStartedwithNode.js
InstallingandrunningNode.jsChoosinganeditorUsinganapplicationframework
GettingstartedwithExpressExploringourExpressapplication
UnderstandingExpressroutesandviewsUsingnodemonforautomaticrestartsCreatingmodularapplicationswithExpressBootstrappinganExpressapplicationUnderstandingExpressmiddleware
ImplementingerrorhandlingUsingExpressmiddleware
Summary3.AJavaScriptPrimer
IntroducingJavaScripttypesJavaScriptprimitivetypes
Functionalobject-orientedprogrammingFunctionalprogramminginJavaScriptUnderstandingscopesinJavaScript
StrictmodeObject-orientedprogramminginJavaScript
ProgrammingwithoutclassesCreatingobjectswiththenewkeyword
ProgrammingwithclassesClass-basedinheritance
Summary4.IntroducingNode.jsModules
OrganizingyourcodebaseJavaScriptmodulesystems
CreatingmodulesinNode.jsDeclaringamodulewithanameanditsownscopeDefiningfunctionalityprovidedbythemoduleImportingamoduleintoanotherscript
Definingadirectory-levelmoduleImplementinganExpressmiddlewaremoduleSummary
5.CreatingDynamicWebsitesHandlinguser-submitteddataCommunicatingviaAjaxImplementingotherdataoperations
ListingdatainviewsIssuingadeleterequestfromtheclientSplittingupExpressviewsusingpartials
Summary6.TestingNode.jsApplications
WritingasimpletestinNode.jsStructuringthecodebasefortestsWritingBDD-styletestswithMocha
Resettingstatebetweentests
UsingChaiforassertionsCreatingtestdoubles
CreatingtestdoublesusingSinon.JSTestinganExpressapplication
SimplifyingtestsusingSuperAgentFull-stacktestingwithPhantomJSSummary
7.SettingupanAutomatedBuildSettingupanintegrationserver
SettingupapublicGitHubrepositoryBuildingaprojectonTravisCI
AutomatingthebuildprocesswithGulpRunningtestsusingGulp
CheckingcodestylewithESLintAutomaticallyfixingissuesinESLintRunningESLintfromGulp
GatheringcodecoveragestatisticsRunningintegrationtestsfromGulpSummary
8.MasteringAsynchronicityUsingthecallbackpatternforasynchronouscode
ExposingthecallbackpatternConsumingasynchronousinterfaces
WritingcleanerasynchronouscodeusingpromisesImplementingpromise-basedasynchronouscode
ConsumingthepromisepatternParallelisingoperationsusingpromises
CombiningasynchronousprogrammingpatternsSummary
9.PersistingDataIntroducingMongoDB
WhychooseMongoDB?ObjectmodelingJavaScriptScalability
GettingstartedwithMongoDBUsingtheMongoDBshell
UsingMongoDBwithExpressPersistingobjectswithMongooseIsolatingpersistencecodeDependencyinjectioninNode.jsProvidingdependenciesRunningdatabaseintegrationtestsonTravisCI
IntroducingRedis
WhyuseRedis?InstallingRedisUsingRedisasakey-valuestoreStoringstructureddatainRedis
BuildingauserrankingsystemwithRedisUsingRedisfromNode.js
Testingwithredis-jsImplementinguserrankingswithRedisMakinguseoftheusersservice
AnoteonsecuritySummary
10.CreatingReal-timeWebAppsUnderstandingoptionsforreal-timecommunicationIntroducingSocket.IO
ImplementingachatroomwithSocket.IOScalingreal-timeNode.jsapplications
UsingRedisasabackendIntegratingSocket.IOwithExpressDirectingSocket.IOmessagesTestingSocket.IOapplicationsOrganizingSocket.IOapplications
Exposingreal-timeupdatestothemodelOrganizingSocket.IOapplicationsusingnamespacesPartitioningSocket.IOclientsusingrooms
Summary11.DeployingNode.jsApplications
WorkingwithHerokuSettingupaHerokuaccountandtoolingRunninganapplicationlocallywithHerokuDeployinganapplicationtoHerokuWorkingwithHerokulogs,config,andservices
SettingupMongoDBSettingupRedis
DeployingfromTravisCISettingencryptedTravisCIenvironmentvariables
InstallingRubyCreatinganencryptedenvironmentvariable
FurtherresourcesSummary
12.AuthenticationinNode.jsIntroducingPassport
ChoosinganauthenticationstrategyUnderstandingthird-partyauthentication
UsingExpresssessions
SpecifyingasessionsecretDecidingwhenthesessiongetssavedUsingalternativesessionstoresUsingsessionmiddleware
ImplementingsocialloginSettingupaTwitterapplicationConfiguringPassportPersistinguserdatawithRedisConfiguringPassportwithpersistenceHidingfunctionalityfromunauthenticatedusersIntegrationtestingwithPassport
AllowinguserstologoutAddingotherloginprovidersSummary
13.CreatingJavaScriptPackagesWritinguniversalmodules
ComparingNode.jsandRequireJSSupportingthebrowserenvironmentUsingAMDmoduleswithRequireJSIsomorphicJavaScript
WritingnpmpackagesDefiningannpmpackage
PublishingapackagetonpmRunningautomatedclientsonthewebReleasingastandalonetooltonpm
UsingNode.jsmodulesinthebrowserControllingBrowserify'soutput
Summary14.Node.jsandBeyond
UnderstandingNode.jsversioningAbriefhistoryofNode.jsIntroducingtheNode.jsLTSschedule
UnderstandingECMAScriptversioningExploringECMAScript2015
UnderstandingES2015modulesUsingsyntaximprovementsfromES2015
Thefor...ofloopThespreadoperatorandrestparametersDestructuringassignment
IntroducinggeneratorsIntroducingECMAScript2016GoingbeyondJavaScript
Exploringcompile-to-JavaScriptlanguagesTypeScript
CoffeeScriptAndbeyond...
IntroducingatrueassemblylanguageforthewebUnderstandingasm.jsUnderstandingWebAssembly
JavaScriptandASP.NETExploring.NETCore
Definingprojectstructurein.NETCoreManagingdependenciesin.NETCoreBuildingwebapplicationsinASP.NETCore
IntegrationwithJavaScriptServer-sideJavaScriptintegrationwith.NET
SummaryIndex
LearningNode.jsfor.NETDevelopersCopyright©2016PacktPublishing
Allrightsreserved.Nopartofthisbookmaybereproduced,storedinaretrievalsystem,ortransmittedinanyformorbyanymeans,withoutthepriorwrittenpermissionofthepublisher,exceptinthecaseofbriefquotationsembeddedincriticalarticlesorreviews.
Everyefforthasbeenmadeinthepreparationofthisbooktoensuretheaccuracyoftheinformationpresented.However,theinformationcontainedinthisbookissoldwithoutwarranty,eitherexpressorimplied.Neithertheauthor,norPacktPublishing,anditsdealersanddistributorswillbeheldliableforanydamagescausedorallegedtobecauseddirectlyorindirectlybythisbook.
PacktPublishinghasendeavoredtoprovidetrademarkinformationaboutallofthecompaniesandproductsmentionedinthisbookbytheappropriateuseofcapitals.However,PacktPublishingcannotguaranteetheaccuracyofthisinformation.
Firstpublished:June2016
Productionreference:1170616
PublishedbyPacktPublishingLtd.
LiveryPlace
35LiveryStreet
BirminghamB32PB,UK.
ISBN978-1-78528-009-2
www.packtpub.com
CreditsAuthor
HarryCummings
Reviewer
DavidSimons
CommissioningEditor
KunalParikh
AcquisitionEditor
RahulNair
ContentDevelopmentEditor
TrushaShriyan
TechnicalEditor
JayeshSonawane
CopyEditor
SafisEditing
ProjectCoordinator
KinjalBari
Proofreader
SafisEditing
Indexer
MariammalChettiyar
Graphics
DishaHaria
AbouttheAuthorHarryCummingshasbeenworkinginsoftwaredevelopmentfor8years,andforthepastfewyears,hehasperformedtheroleoftechnicalleadacrossavarietyofprojectsforvariedclients.Hehas,inthepast,alsoworkedasadeveloper,projectmanager,andconsultant.Thisgiveshimanexcellentall-roundviewoftheroleofatechnicalleadanditsrelationshipwithotherrolesaswellasinsightintoeverystageofprojectdelivery,frominitialanalysistolong-termmaintenance.
HarryhasextensiveexperienceinC#/.NET,JavaandScala,andJavaScript/Node.js.Hecontinuestoworkdirectlywiththesetechnologiesonaregularbasisintheteamsthatheleads.Hisbroaderinterestsandexpertiselieinsharingandnurturingsoftwaredevelopmentbestpracticesthroughtrainingandmentoring.HehasappearedatconferencessuchasNDCLondonandSDDConf,speakingaboutdiversetopics,rangingfromintroductoryNode.jsthroughtoautomatedteststrategiesandlong-termprojectmaintainability.
AbouttheReviewerDavidSimonsisaLondon-basedsoftwareconsultant.Heisfamiliarwithawiderangeoftools,havinghelpedclientssuchastheBBCandNewsInternationaldeliverwebsolutionsinarangeoflanguages,including.NET,Java,andfull-stackJavaScript.Heshareshisinsightsaroundtheseandhisbackgroundinstatisticsresearchatarangeofconferences,includingNDCandJSConf.
Asof2016,heworkswithLondon-basedconsultancyGraphAwaretoadvocateandconsultontheuseofgraphdatabasesinmodernapplications.
eBooks,discountoffers,andmoreDidyouknowthatPacktofferseBookversionsofeverybookpublished,withPDFandePubfilesavailable?YoucanupgradetotheeBookversionatwww.PacktPub.comandasaprintbookcustomer,youareentitledtoadiscountontheeBookcopy.Getintouchwithusat<[email protected]>formoredetails.
Atwww.PacktPub.com,youcanalsoreadacollectionoffreetechnicalarticles,signupforarangeoffreenewslettersandreceiveexclusivediscountsandoffersonPacktbooksandeBooks.
https://www2.packtpub.com/books/subscription/packtlib
DoyouneedinstantsolutionstoyourITquestions?PacktLibisPackt'sonlinedigitalbooklibrary.Here,youcansearch,access,andreadPackt'sentirelibraryofbooks.
Whysubscribe?FullysearchableacrosseverybookpublishedbyPacktCopyandpaste,print,andbookmarkcontentOndemandandaccessibleviaawebbrowser
PrefaceThepurposeofthisbookistohelp.NETorJavadevelopersmaketheleaptoNode.js.Youmayhavesomewebdevelopmentexperience,andperhapsyou'vewrittensomebrowser-basedJavaScriptinthepast.ItmightnotbeobviouswhyanyonewouldwanttotakeJavaScriptoutofthebrowseranduseitforserver-sidedevelopment.However,thisisexactlywhatNode.jsdoes.What'smore,Node.jshasbeenaroundforlongenoughnowtohavematuredasaplatform,andhassustaineditsimpressivegrowthinpopularitywellbeyondanyperiodthatcouldbeattributedtoinitialhypeoveranewtechnology.
ThefirstobjectiveofthisbookthenistoexplainwhyNode.jsisacompellingtechnologythat'sworthlearningmoreabout.ThefirstfewchaptersintroduceNode.jswiththisinmind,quicklygetyouupandrunningwithNode.js,andprovideanimportant(re)introductiontotheJavaScriptlanguagetosetyouontherighttrack.
ThemainpartofthisbookwillthentakeyouthroughaworkedexampleofbuildingupaNode.jsweb-applicationstepbystep.Intheprocess,we'llillustratealltheimportanttoolsandtechniquesrequiredforreal-worlddevelopmentprojectsinNode.js.TheaimistomakethemostofyourexistingdevelopmentexpertisetoallowyoutoquicklyreachthesamelevelofbestpracticesandprofessionalismwithNode.js.
ThefinalchaptersofthebookshowhowtouseNode.jsforotherpurposesoutsideofwebapplicationsandhowtocontinuelearningNode.jsandexploringtheecosystemaroundit.We'llalsoseehowyoucanuseNode.jsalongside.NETandbenefitfromapplyingyourprogrammingskillsacrossbothtechnologies.
WhatthisbookcoversChapter1,WhyNode.js?,introducesNode.jsasaprogrammingplatform.ItcoverstheexecutionmodelofNode.js,particularlyhowitdiffersfrom.NETandJava,andtheusecasesinwhichthesedifferencesbecomestrengths.ThischapteralsodiscussesthesuitabilityofJavaScriptasadevelopmentlanguage.
Chapter2,GettingStartedwithNode.js,divesstraightintocreatingaNode.jsapplication.Inthischapter,youwillinstallNode.js,chooseacodeeditor,andsetupaminimalwebapplicationproject.You'llalsolearnsomeimportantcommand-linetoolsforworkingwithNode.js.
Chapter3,AJavaScriptPrimer,introducesthemostimportantthingstoknowwhenprogramminginJavaScript.ItdescribestheJavaScripttypesystemanditsparticularflavoroffunctionalobject-orientedprogramming,includingprototype-basedinheritance.ThischapteralsocoversafewkeygotchasandJavaScriptlanguagequirks.
Chapter4,IntroducingNode.jsModules,explainshowtostructureJavaScriptapplicationsusingmodules.ItintroducestheNode.jsmodulesystemandshowsyouhowtousethistoorganiseyourapplication'scode.
Chapter5,CreatingDynamicWebsites,expandsontheexamplesfromthepreviouschaptertobuildafunctioningwebapplication.You'lladdaJSONAPIanddynamicviewstoyourapplicationandcommunicatebetweentheclientandserverusingAjax.
Chapter6,TestingNode.jsApplications,showsyouhowtowriteautomatedtestsinJavaScriptandNode.js.ItintroducesanumberoftoolsandlibrariesforwritingandrunningtestsinJavaScript,andguidesyouthroughwritingavarietyofunittestsandintegrationtestsforyourapplication.
Chapter7,SettingupanAutomatedBuild,coversbuildautomationandcontinuousintegrationinNode.js.You'llsetupaCIserverandtaskrunnerforyourapplication,addingautomatedtaskstoruntests,executestaticanalysis,andassesscodecoverage.
Chapter8,MasteringAsynchronicity,introducesdifferentpatternsforasynchronousprogramminginJavaScript.You'llapplythesetoyourownapplicationandmakethemostofJavaScriptlanguagefeaturesandlibrariesforsimplifyingasynchronouscode.
Chapter9,PersistingData,explainspersistentdatastoresthatcanbeusedwithNode.js.ItintroducesMongoDBandRedis,explainingtheirdifferentdatamodelsandtheirusecases.You'llintegratebothofthesedatastoreswithyourNode.jsapplication.
Chapter10,CreatingReal-timeWebApps,showshowtoimplementreal-timetwo-waycommunicationbetweentheclientandtheserver.You'llusetheSocket.IOlibrarytoaddreal-timefunctionalityintoyourapplication.You'llalsoseehowtowritetestsforthisfunctionalityand
howtowritescalablereal-timeapplicationsusingRedisasabackend.
Chapter11,DeployingNode.jsApplications,demonstrateshowtogetaNode.jsapplicationontotheWeb.You'lldeployyourapplicationtoafreecloud-hostingprovider.You'llseehowtoconfiguredatastoresandhowtouseremoteserverlogsfordebugging.
Chapter12,AuthenticationinNode.js,coversauthenticationforNode.jswebapplications.You'llimplementauthenticationusingthird-partyproviders,integratethiswithyourapplication,andshowdifferentcontenttologged-inandlogged-outusers.
Chapter13,CreatingJavaScriptPackages,explainshowtocreatestandaloneJavaScriptpackagesforusebyothers.You'llseehowtowriteuniversalJavaScriptlibrariesthatcanrunonboththeclientandtheserver,andhowtowriteastandalonecommand-lineapplicationusingNode.js.
Chapter14,Node.jsandBeyond,putsthecontentofthisbookinawidercontext.ItexplainshowNode.jsandJavaScriptarecontinuingtoevolve,soyoucanbepreparedforandtakeadvantageofupcomingchanges.ItcoverssomealternativeprogramminglanguagesforNode.jsandtheWeb,andhowtheserelatetoJavaScript.ItdiscusseshowsomeoftheprinciplesfromNode.jscanbeappliedto.NETprogramming,andillustrateshowtheseareparticularlyvisiblein.NETCore(thenewversionof.NET).ItalsoshowshowyoucanuseNode.jstogetherwith.NETtogainthebestofbothworlds.
WhatyouneedforthisbookAllofthetoolsandservicesusedinthisbookareavailableforfreeonline.Mostoftheworkedexamplesrequireanactivewebconnectionatsomepoint.Togetstarted,youneednothingmorethanaconsole,awebbrowser,andpermissiontoinstallnewsoftwareonyourmachine.Tosupportdeveloperscomingfroma.NETbackground,someoftheconsolelistingsorexamplestepsinthisbookuseWindowsconventions(forexample,backslashesinpaths).NoneoftheexamplesdependonWindowsspecificallythough.YoucanworkthroughthisbookonWindows,MacOSX,orLinux.
WhothisbookisforThisbookisfor.NETorJavadeveloperswhoareinterestedinlearningNode.js.NopriorexperiencewithNode.jsisexpected.Youmighthavewrittensomeclient-sideJavaScriptbefore,butthisisnotrequired.ThemainworkedexampleinthisbookisaNode.jswebapplication.Webdevelopmentexperiencein.NETorJavawillbehelpful,butit'snotnecessarytohaveexperiencewithanyparticularapplicationlibraryorframework.
ConventionsInthisbook,youwillfindanumberoftextstylesthatdistinguishbetweendifferentkindsofinformation.Herearesomeexamplesofthesestylesandanexplanationoftheirmeaning.
Codewordsintext,databasetablenames,foldernames,filenames,fileextensions,pathnames,dummyURLs,userinput,andTwitterhandlesareshownasfollows:"ES2015introducestheletkeywordfordeclaringvariables."
Ablockofcodeissetasfollows:
<!DOCTYPEhtml>
<html>
<head>
<title>{{title}}</title>
<linkrel='stylesheet'href='/stylesheets/style.css'/>
</head>
<body>
<h1>{{title}}</h1>
<p>Welcometo{{title}}</p>
</body>
</html>
Whenwewishtodrawyourattentiontoaparticularpartofacodeblock,therelevantlinesoritemsaresetinbold:
/*GEThomepage.*/
router.get('/',function(req,res,next){
res.render('index',{title:'Express',name:'World'});
});
Anycommand-lineinputoroutputiswrittenasfollows:
>npminstall–gnodemon
Newtermsandimportantwordsareshowninbold.Wordsthatyouseeonthescreen,forexample,inmenusordialogboxes,appearinthetextlikethis:"ClickingtheNextbuttonmovesyoutothenextscreen."
Note
Warningsorimportantnotesappearinaboxlikethis.
Tip
Tipsandtricksappearlikethis.
ReaderfeedbackFeedbackfromourreadersisalwayswelcome.Letusknowwhatyouthinkaboutthisbook—whatyoulikedordisliked.Readerfeedbackisimportantforusasithelpsusdeveloptitlesthatyouwillreallygetthemostoutof.
Tosendusgeneralfeedback,simplye-mail<[email protected]>,andmentionthebook'stitleinthesubjectofyourmessage.
Ifthereisatopicthatyouhaveexpertiseinandyouareinterestedineitherwritingorcontributingtoabook,seeourauthorguideatwww.packtpub.com/authors.
CustomersupportNowthatyouaretheproudownerofaPacktbook,wehaveanumberofthingstohelpyoutogetthemostfromyourpurchase.
DownloadingtheexamplecodeYoucandownloadtheexamplecodefilesforthisbookfromhttps://github.com/NodeJsForDevelopersandalsofromyouraccountathttp://www.packtpub.com.Ifyoupurchasedthisbookelsewhere,youcanvisithttp://www.packtpub.com/supportandregistertohavethefilese-maileddirectlytoyou.
Youcandownloadthecodefilesbyfollowingthesesteps:
1. Loginorregistertoourwebsiteusingyoure-mailaddressandpassword.2. HoverthemousepointerontheSUPPORTtabatthetop.3. ClickonCodeDownloads&Errata.4. EnterthenameofthebookintheSearchbox.5. Selectthebookforwhichyou'relookingtodownloadthecodefiles.6. Choosefromthedrop-downmenuwhereyoupurchasedthisbookfrom.7. ClickonCodeDownload.
YoucanalsodownloadthecodefilesbyclickingontheCodeFilesbuttononthebook'swebpageatthePacktPublishingwebsite.Thispagecanbeaccessedbyenteringthebook'snameintheSearchbox.PleasenotethatyouneedtobeloggedintoyourPacktaccount.
Oncethefileisdownloaded,pleasemakesurethatyouunziporextractthefolderusingthelatestversionof:
WinRAR/7-ZipforWindowsZipeg/iZip/UnRarXforMac7-Zip/PeaZipforLinux
DownloadingthecolorimagesofthisbookWealsoprovideyouwithaPDFfilethathascolorimagesofthescreenshots/diagramsusedinthisbook.Thecolorimageswillhelpyoubetterunderstandthechangesintheoutput.Youcandownloadthisfilefromhttp://www.packtpub.com/sites/default/files/downloads/LearningNodejsForNETDevelopers_ColorImages.pdf
ErrataAlthoughwehavetakeneverycaretoensuretheaccuracyofourcontent,mistakesdohappen.Ifyoufindamistakeinoneofourbooks—maybeamistakeinthetextorthecode—wewouldbegratefulifyoucouldreportthistous.Bydoingso,youcansaveotherreadersfromfrustrationandhelpusimprovesubsequentversionsofthisbook.Ifyoufindanyerrata,pleasereportthembyvisitinghttp://www.packtpub.com/submit-errata,selectingyourbook,clickingontheErrataSubmissionFormlink,andenteringthedetailsofyourerrata.Onceyourerrataareverified,yoursubmissionwillbeacceptedandtheerratawillbeuploadedtoourwebsiteoraddedtoanylistofexistingerrataundertheErratasectionofthattitle.
Toviewthepreviouslysubmittederrata,gotohttps://www.packtpub.com/books/content/supportandenterthenameofthebookinthesearchfield.TherequiredinformationwillappearundertheErratasection.
PiracyPiracyofcopyrightedmaterialontheInternetisanongoingproblemacrossallmedia.AtPackt,wetaketheprotectionofourcopyrightandlicensesveryseriously.IfyoucomeacrossanyillegalcopiesofourworksinanyformontheInternet,pleaseprovideuswiththelocationaddressorwebsitenameimmediatelysothatwecanpursuearemedy.
Pleasecontactusat<[email protected]>withalinktothesuspectedpiratedmaterial.
Weappreciateyourhelpinprotectingourauthorsandourabilitytobringyouvaluablecontent.
QuestionsIfyouhaveaproblemwithanyaspectofthisbook,youcancontactusat<[email protected]>,andwewilldoourbesttoaddresstheproblem.
Chapter1.WhyNode.js?Node.jsisstillrelativelynewcomparedtoplatformssuchas.NETandJava,buthasbecomeverypopularinashorttime,andhasevenstartedinfluencingtheseplatforms.Thisisthankstoitsdistinctiveprogrammingmodel,extensiveecosystem,andpowerfultooling.
ThesefactorsmakeNode.jsacompellingalternativetootherplatforms.Theycanalsomakeitintimidating.Itsdistinctiveprogrammingmodelmayseemquitealiencomparedtootherplatforms.Thesheerrangeofavailablelibrariesandtoolscanbebewildering.
ThisbookwillguideyouthroughNode.jssoyoucanstartusingitinyourapplications.ItwillhelpyoutounderstandNode.js,navigateitsecosystem,andleverageyourexistingdevelopmentskillsinthisnewenvironment.
Inthischapter,wewillcoverthefollowingtopics:
IntroducingtheNode.jsplatformSeeinghowitsexecutionmodelworksExploringtheNode.jsecosystemLookingatJavaScriptasalanguagechoiceConsideringtherangeofusecasesforNode.js
WhatisNode.js?Node.jsconsistsofaJavaScriptenginetogetherwithlow-levelAPIsforcoreserver-sidefunctionality.TheexecutionengineisthesameV8enginedevelopedfortheChromewebbrowser.Node.jstakesthisengineandembedsitinastandaloneapplicationthatcanrunJavaScriptoutsidethebrowser.
InNode.js,thestandardAPIsfoundinbrowserstosupportclient-sidewebdevelopment,suchastheDocumentObjectModel(DOM)andXMLHttpRequest,arenotpresent.Instead,thereareAPIstosupportgeneral-purposeapplicationdevelopment.ThesecoreAPIscoverlow-levelfunctionalitysuchasthefollowing:
NetworkingandsecurityAccessingthefilesystemDefiningandrequiringmodulesRaisingandconsumingeventsHandlingbinarydatastreamsCompressionUTF-8supportRetrievingbasicinformationabouttheOSManagingchildprocesses
SomeoftheseAPIsmayalreadybefamiliarfromdevelopingclient-sideJavaScript.Forexample,theTimersAPIexposesthefamiliarsetTimeoutandsetIntervalfunctions.
Node.jsalsoprovidesseveraltoolstohelpwiththedevelopmentprocess.Theseincludeconsolelogging,debugging,aRead-Eval-PrintLoop(REPL)(orinteractiveconsole),andbasicassertionsfortesting.
UnderstandingtheNode.jsexecutionmodelTheexecutionmodelofNode.jsfollowsthatofJavaScriptinthebrowser.Itisquitedifferentfromthatofmostgeneral-purposeprogrammingplatforms.
Statedformally,Node.jshasasingle-threaded,non-blocking,event-drivenexecutionmodel.Wewilldefineeachofthesetermsinthissection.
Non-blocking
Putsimply,Node.jsrecognizesthatmanyprogrammesspendmostoftheirtimewaitingforotherthingstohappen,forexample,slowI/Ooperationssuchasdiskaccessandnetworkrequests.
Node.jsaddressesthisbymakingtheseoperationsnon-blocking.Thismeansthatprogramexecutioncancontinuewhiletheyhappen.Forexample,thefilesystemAPI'sstatfunctionforretrievingstatisticsaboutafilemaybecalledasfollows:
fs.stat('/hello/world',function(error,stats){
console.log('Filelastupdatedat:'+stats.mtime);
});
Twoargumentsarepassedtothefs.statfunction:thenameofthefilethatweareinterestedin,andacallbackfunction.Thefs.statcallreturnsimmediately,returningcontrolofexecutiontothecurrentthreadbutnotreturningavalue.Iftherearefurthercommandsfollowingthefs.statcall,thesewillthenbeexecuted.Otherwise,thethreadisreleasedtoperformotherwork.Thecallbackfunctionisinvoked(thatis'calledback')onlyaftertheruntimehasfinishedcommunicatingwiththefilesystem.Theresultofthefilesystemoperationispassedintothecallbackfunction.
Thisnon-blockingapproachisalsocalledasynchronousprogramming.Otherplatformssupportthis(forexample,C#'sasync/awaitkeywordsand.NET'sTaskParallelLibrary).However,itisbakedintoNode.jsinawaythatmakesitsimpleandnaturaltouse.AsynchronousAPImethodsareallcalledinthesamewayasfs.stat.Theyalltakeacallbackfunctionthatgetspassederrorandresultarguments.
Event-driven
Theevent-drivennatureofNode.jsdescribeshowoperationsarescheduled.Intypicalproceduralenvironments,aprogramhasanentrypointthatexecutesasetofcommandsuntilcompletion,orentersaloopandperformssomeprocessingoneachiteration.
Node.jshasabuilt-ineventloop,whichisn'texposedtothedeveloper.Itisthejoboftheeventlooptodecidewhichpieceofcodetoexecutenext.Typically,thiswillbeacallbackfunctionthatisreadytoruninresponsetosomeotherevent.Forexample,afilesystemoperationmayhavecompleted,atimeoutmayhaveexpired,oranewnetworkrequestmayhavearrived.
Thisbuilt-ineventloopsimplifiesasynchronousprogrammingbyprovidingaconsistentapproach
andavoidingtheneedforapplicationstomanagetheirownscheduling.
Single-threaded
Thesingle-threadednatureofNode.jssimplymeansthatthereisonlyonethreadofexecutionineachprocess.Also,eachpieceofcodeisguaranteedtoruntocompletionwithoutbeinginterruptedbyotheroperations.Thisgreatlysimplifiesdevelopmentandmakesprogramseasiertoreasonabout.Itremovesthepossibilityforarangeofconcurrencyissues.Forexample,itisnotnecessarytosynchronize/lockaccesstosharedin-processstateasitisinJavaor.NET.Aprocesscan'tdeadlockitselforcreateraceconditionswithinitsowncode.Single-threadedprogrammingisonlyfeasibleifthethreadnevergetsblockedwaitingforlong-runningworktocomplete.Thus,thissimplifiedprogrammingmodelismadepossiblebythenon-blockingnatureofNode.js.
IntroducingtheNode.jsecosystemThebuilt-inNode.jsAPIsprovidealow-levelcoreforcreatingapplications.ApplicationstypicallyonlyuseasmallnumberoftheseAPIsdirectly.Theyoftenusethird-partylibrarymodulesthatprovidehigher-levelabstractionsforapplicationdevelopment.
Node.jshasitsownpackagemanager,npm.Thisissimilarto.NET'sNuGetorthepackagemanagementaspectsofJava'sMaven.ApplicationsspecifytheirdependenciesinasimpleJSONfile.
Thenpmregistryprovidesacentralrepositoryforpackages.Thisregistryhasgrownrapidlyandisalreadymuchlarger(intermsofnumberofavailablepackages)thanthecorrespondingrepositoriesforotherplatforms(seehttp://www.modulecounts.com/).Therearehundredsofthousandsofpackagesavailable,providingavastarrayoffunctionality.
Thenpmcommandlinetoolcanbeusedtodownloadpackagesandinstallnewones.Librarydependenciesareinstalledlocallytoeachapplication.Somepackagesprovidecommand-linetools,whichmaybeinstalledgloballyratherthanunderaspecificproject.
Manyframeworksavailableonnpmaresplitintoasmallextensiblecoreandanumberofcomposablemodules.Thisapproachmakesiteasytounderstandthelibrariesonwhichyourapplicationdepends,avoidingtheneedtoreasonaboutcomplexheavyweightframeworks.
Theconsistencyofcallingnon-blocking(asynchronous)APImethodsinNode.jscarriesthroughtoitsthird-partylibraries.Thisconsistencymakesiteasytobuildapplicationsthatareasynchronousthroughout.
WhyJavaScript?JavaScriptisalanguagethatcanseemunintuitivecomparedtootherpopularobject-oriented(OO)languages.Italsohasanumberofquirksandflawsthathavedrawncriticism(andoccasionalridicule).Itmightthenseemasurprisingchoiceoflanguageforanewprogrammingplatform.ThissectiondiscussesthefactorsthatmakeJavaScriptamoreappealingchoice.
AclearcanvasThesizeandcomplexityofJavaScriptispartofitsappeal.Thecorelanguageitself,whichdoesn'tincludeAPIssuchastheDOM,issmallandsimple.ThismakesiteasyforNode.jstoestablishitsownstylesandconventions.
ThenewAPIsprovidedbyNode.jsandtheconsistentapproachtoasynchronousprogrammingwouldn'tbepossibleinamorecomplexlanguagewithalargerpre-existingstandardclasslibrary.
FunctionalnatureJavaScriptwasfirstbuiltasaprogramminglanguageforclient-sidefunctionalityinthebrowser.Thismightnotmakeitanobviouschoiceforgeneral-purposeprogramming.
Infact,thesetwousecasesdohavesomethingimportantincommon.Userinterfacecodeisnaturallyevent-driven(forexample,bindingeventhandlerstobuttonclicks).Node.jsmakesthisavirtuebyapplyinganevent-drivenapproachtogeneral-purposeprogramming.
JavaScriptsupportsfunctionsasfirst-classobjects.Thismeansit'seasytocreatefunctionsdynamicallyandpassaroundreferencestothem.Thisfitsinwellwiththeasynchronous,non-blockingapproachofNode.js.Inparticular,it'seasytoexposeanduseAPIsbasedaroundcallbackfunctions.
AbrightfutureJavaScripthasreceivedalotofattentioninthelastseveralyearsasithasbecomemorewidelyusedforprovidingrichfunctionalityontheWeb.BrowservendorshaveputahugeamountofengineeringeffortintoimprovingtheperformanceofJavaScript.Node.jsbenefitsfromthisdirectlyviaitsuseofChrome'sV8engine.
TheJavaScriptlanguageitselfisundergoingsomemajorchangesforthebetter.TheECMAScript2015standard(previouslyknownasES6)representsthemostsignificantrevisionofthelanguageinitshistory.Itintroducesfeaturesthatmakethelanguagemoreintuitiveandlessverbose.ItalsoaddressesflawsthatJavaScripthasbeencriticizedforinthepast,removinggotchasandmakingprogramseasiertoreasonabout.
WhentouseNode.jsAsdiscussedearlierinthischapter,Node.jsrecognizesthatI/Oisabottleneckformanyapplications.Onmostprogrammingplatforms,threadswillwastetimeblockingonI/Ooperations.Thereareapproachesdeveloperscantaketoavoidthis,buttheseallinvolveaddingsomecomplexitytotheircode.InNode.js,theplatformitselfprovidesacompletelynaturalapproach.
WritingwebapplicationsTheflagshipusecaseforNode.jsisbuildingwebapplications.Theseareinherentlyevent-drivenasmostorallprocessingtakesplaceinresponsetoHTTPrequests.Also,manywebsitesdolittlecomputationalheavy-liftingoftheirown.TheytendtoperformalotofI/Ooperations:
StreamingrequestsfromtheclientTalkingtoadatabase,locallyoroverthenetworkPullingindatafromremoteAPIsoverthenetworkReadingfilesfromdisktosendbacktotheclient
ThesefactorsmakeI/Ooperationsalikelybottleneckforwebapplications.Thenon-blockingprogrammingmodelofNode.jsallowswebapplicationstomakethemostofasinglethread.AssoonasanyoftheseI/Ooperationsstarts,thethreadisimmediatelyfreetopickupandstartprocessinganotherrequest.ProcessingofeachrequestcontinuesviaasynchronouscallbackswhenI/Ooperationscomplete.Theprocessingthreadisonlykickingoffandlinkingtogethertheseoperations,neverwaitingforthemtocomplete.ThisallowsNode.jstohandleamuchhigherrateofrequestsperthreadthanotherplatforms.Youcanalsostillmakeuseofmultiplethreads(forexample,onmulti-coreCPUs)bysimplyrunningmultipleinstancesoftheNode.jsprocess.
IdentifyingotherusecasesThereareofcoursesomeapplicationsthatdon'tperformmuchI/OandaremorelikelytobeCPUbound.Node.jswouldbelesssuitableforcomputationally-intensiveapplications.Programsthatdoalotofprocessingofin-memorydataarelessconcernedaboutI/O.
WebapplicationsarenottheonlyI/O-heavyapplicationsthough.OtherclassesofprogramthatcouldbeagoodcandidateforNode.jsincludethefollowing:
ToolsthatmanipulatelargeamountsofdataondiskSupervisorprogramscoordinatingothersoftwareorhardwareNon-browserGUIapplicationsthatneedtorespondtouserinput
Node.jsisespeciallysuitableforglueapplicationsthatpulltogetherfunctionalityfromotherremoteservices.Theincreasingpopularityofmicroservicesasanarchitecturalpatternmakesthiskindofapplicationmorecommon.
Whynow?Node.jshasbeenaroundforseveralyears,butnowistheperfecttimetostartusingitifyouhaven'talready.
ThereleaseofNode.jsv4towardstheendof2015consolidatedtheproject'sgovernancemodelandheraldsNode.jscomingtomaturity.ItalsoallowstheprojecttokeepmoreuptodatewiththeV8engine.ThismeansthatNode.jscanbenefitmoredirectlyfromongoingdevelopmentonV8.Forexample,securityandperformanceimprovementstoV8willnowmaketheirwayintoNode.jsmuchsooner.
Asdiscussedearlierinthischapter,thereleaseoftheECMAScript2015standardmakesJavaScriptamuchmoreappealinglanguage.ItpullsinusefulfeaturesfromotherpopularOOlanguagesandresolvesanumberoflong-standingflawsinJavaScript.
Meanwhile,theecosystemofthirdpartylibrariesandtoolsaroundNode.jsandJavaScriptcontinuestogrow.Node.jsistreatedasafirst-classcitizenbymajorhostingplatforms.CompaniessuchasGoogleandMicrosoftarealsothrowingtheirweightbehindJavaScriptandrelatedtechnologies.
SummaryInthischapter,wehaveunderstoodNode.jsanditsdistinctiveexecutionmodel,exploredthegrowingecosystemaroundNode.jsandJavaScript,seenthereasonsforJavaScriptasalanguagechoice,anddescribedthekindsofapplicationthatcanbenefitfromNode.js.
NowthatyouknowhowNode.jsworksandwhentouseit,it'stimetodiveinandgetourfirstNode.jsapplicationupandrunning.
Chapter2.GettingStartedwithNode.jsThischapterwillgetyouupandrunningwithNode.js.You'llseehowquickthiscanbeandhoweasyitistostartwritingwebapplications.You'llalsochooseadevelopmentenvironmentforworkingwithNode.js.Inthischapter,wewillcoverthefollowingtopics:
InstallingNode.jsWritingourfirstNode.jswebapplicationSettingupourdevelopmentenvironment
InstallingandrunningNode.jsToinstallNode.js,visithttps://nodejs.org,anddownloadandruntheinstallerpackageforthecurrentlyrecommendedversion.TheexamplesinthisbookarebasedonNode.jsv6,releasedinApril2016andsupportedthroughtoApril2018.
Afterinstallation,openupaconsolewindow(runcommandpromptonWindows,orterminalonMac)andtypenode.
ThisopenstheNode.jsREPL,whichworksliketheJavaScriptconsoleinbrowsers.Trytypinginafewcommandsandseetheoutput:
>functionsquare(x){returnx*x;}
undefined
>square(42)
1764
>newDate()
2016-05-02T16:08:41.915Z
>varfoo={bar:'baz'}
undefined
>typeoffoo
'object'
>foo.bar
'baz'
Nowlet'smakeuseofoneoftheNode.js-specificAPIstocreateanHTTPserver.TypethefollowingcommandsintotheREPL(theoutputofeachcommandisomittedfromthelistingbelowforbrevity):
>varlistener=function(request,response){response.end('HelloWorld!')}
>require('http').createServer(listener).listen(3000)
Nowtryvisitinghttp://localhost:3000inyourbrowser.Congratulations!Youhavewrittenyourfirstwebserver,injusttwolinesofcode.ThefirstlinedefinesacallbackfunctionforhandlingHTTPrequestsandreturningaresponse.ThesecondlinesetsupanewserverthatacceptsHTTPrequestsonport3000andinvokesourcallbackfunctionforeachrequest.
YoucanexittheNode.jsREPLbytypingprocess.exit().
ChoosinganeditorOfcourse,we'renotgoingtowriteallofourcodeinsidetheREPL.YoucanuseanytexteditororIDEyoulikeforwritingJavaScriptforNode.js.Ifyou'renotsurewhattouse,tryoneofthefollowing:
Atom(https://atom.io/)VisualStudioCode(https://code.visualstudio.com/)
Thesearebothfree,lightweightIDEsthatareactuallyimplementedinNode.js.TheyarebothavailableforWindows,Mac,andLinux.
ThecodelistingsintherestofthisbookwillbeJavaScriptsourcecodefiles,notcommandstobetypedintotheREPL.
UsinganapplicationframeworkTheserverwecreatedintheREPLusedthelow-levelHTTPmodulebuiltintoNode.js.ThisprovidesanAPIforcreatingaserverthatreadsdatafromrequestsandwritestoresponses.
Aswithotherprogrammingplatforms,thereareframeworksavailableprovidingmoreusefulhigh-levelabstractionsforwritingwebapplications.TheseincludethingssuchasURLroutingandtemplatingengines.ASP.NETMVC,RubyonRails,andSpringMVCareallexamplesofsuchframeworksondifferentplatforms.
Note
Examplecode
Ifyougetstuckatanypointinthisbook,youcanfollowalongwiththecodeathttps://github.com/NodeJsForDevelopers(thereisarepositoryforeachchapterandacommitforeachheadingthatintroducesanynewcode).
Inthisbook,we'llbeusingaframeworkcalledExpresstowriteawebapplicationinNode.js.ExpressisthemostpopularwebapplicationframeworkforNode.js.Itiswellsuitedtosmall-scaleapplicationssuchastheonewe'llbebuilding.Italsoprovidesagoodintroductiontoimportantconcepts.MostotherpopularNode.jswebapplicationframeworksareconceptuallysimilartoExpress,andseveralareactuallybuiltontopofit.
GettingstartedwithExpressTogetourExpress-basedapplicationstarted,we'llusenpmtoinstalltheexpress-generatorpackage,whichwillcreateaskeletonapplicationbasedonExpress.Runthefollowingcommandintheconsole(thatis,yourregularterminal,notinsidetheNode.jsREPL):
>npminstall-gexpress-generator@~4.x
The-goptioninstallstheExpressgeneratorglobally,soyoucanrunitfromanywhere.Thenextcommandwerunwillcreateanewfoldertocontainourapplicationcode,sorunthiscommandwhereveryouwantthisfoldertoreside:
>express--hoganchapter02
Note
Templatingengines
Expressoffersachoiceoftemplatingengines.We'llbeusingHogan,whichisanimplementationoftheMustachetemplatingengine.YoumayalreadybefamiliarwithMustachefromclient-sidelibraries.Don'tworryifnot,though.It'sverysimpletopickup.
Asyoucanseefromtheoutput,thissetsupaminimalstandardapplicationstructureforus.Nowrunthefollowingcommand(asinstructedbythegeneratoroutput)toinstallthemodulesonwhichourapplicationdepends:
>cdchapter02
>npminstall
ThegeneratorhascreatedaskeletonNode.jswebapplicationforus.Let'stryrunningthis:
>npmstart
Nowvisithttp://localhost:3000againandyou'llseetheExpresswelcomepageasshownhere:
ExploringourExpressapplicationLet'slookatthefoldersthattheExpressgeneratorcreatedforus:
node_modules:Thisfoldercontainsthethird-partypackagesthatourapplicationdependson,whichareinstalledwhenwerunnpminstall(itiscommontoexcludethisdirectoryfromsourcecontrol)public:Thisfoldercontainsthestaticassetsofourapplication:images,client-sideJavaScript,andCSSroutes:Thisfoldercontainsthelogicofourapplicationviews:Thisfoldercontainstheserver-sidetemplatesforourapplication
Therearealsosomefilesthataren'tcontainedinanyoftheprecedingfolders:
package.json:Thisfilecontainsmetadataaboutourapplicationusedbythenpminstallandnpmstartcommandsusedearlier.We'llexplorethisfilefurtherinChapter4,IntroducingNode.jsModules.app.js:Thisfileisthemainentrypointforourapplication,whichgluestogetheralloftheprecedingcomponentsandinitializesExpress.We'llgothroughthisfileinmoredetaillateroninthischapter.bin/www:ThisfileisaNode.jsscriptthatlaunchesourapplication.Thisisthescriptthatgetsexecutedwhenwerunnpmstart.
It'snotimportanttounderstandeverythinginthebin/wwwscriptatthispoint.However,notethatitusesthesamehttp.createServercallasintheREPLexamplebefore.Thistime,though,thelistenerargumentisnotasimplefunctionbutisourentireapplication(definedinapp.js).
UnderstandingExpressroutesandviewsRoutesinExpresscontainthelogicforhandlingrequestsandrenderingtheappropriateresponse.TheyhavesimilarresponsibilitiestocontrollersinMVCframeworkssuchasASP.NET,SpringMVC,orRubyonRails.
Theroutethatservesthepagewejustviewedinthebrowsercanbefoundatroutes/index.jsandlookslikethis:
varexpress=require('express');
varrouter=express.Router();
/*GEThomepage.*/
router.get('/',function(req,res,next){
res.render('index',{title:'Express'});
});
module.exports=router;
TherequirecallimportstheExpressmodule.WewilldiscusshowthisworksinmuchmoredetailinChapter4,IntroducingNode.jsModules.Fornow,thinkofitlikeausingorimportstatementin.NETorJava.Thecalltoexpress.Router()createsacontextunderwhichwecandefinenewroutes.Wewilldiscussthisinmoredetaillateroninthischapter(seeCreatingmodularapplicationswithExpress).Therouter.get()calladdsanewhandlertothiscontextforGETrequeststothepath'/'.
Thecallbackfunctiontakesarequestandresponseargument,similartothelistenerinour"HelloWorld!"serveratthebeginningofthischapter.However,therequestandresponseinthiscaseareobjectsprovidedbyExpress,withadditionalfunctionality.
Therenderfunctionallowsustorespondwithatemplate,whichisrenderedusingthedatawepasstoit.Thisistypicallythelastthingyouwilldoinaroute'scallbackfunction.Here,wepassanobjectcontainingthetitleExpresstotheviewtemplate.
Theviewtemplatecanbefoundatviews/index.hjsandlookslikethis:
<!DOCTYPEhtml>
<html>
<head>
<title>{{title}}</title>
<linkrel='stylesheet'href='/stylesheets/style.css'/>
</head>
<body>
<h1>{{title}}</h1>
<p>Welcometo{{title}}</p>
</body>
</html>
ThisisaHogantemplate.Asmentionedpreviously,HoganisanimplementationofMustache,a
verylightweighttemplatinglanguagethatlimitstheamountoflogicinviews.YoucanseethefullsyntaxofMustacheathttps://mustache.github.io/mustache.5.html.
OurtemplateisasimpleHTMLpagewithsomespecialtemplatetags.The{{title}}tagsarereplacedwiththetitlefieldfromthedatapassedinbytheroute.
Let'schangetheheadingintheviewtoincludeanameaswellasatitle.Itshouldlooklikethis:
<h1>Hello,{{name}}!</h1>
Tryreloadingthepageagain.Youshouldseethefollowing:
Wedon'thaveanameyet.That'sbecausethereisnonamefieldinourviewdata.Let'sfixthatbyeditingourroute:
varexpress=require('express');
varrouter=express.Router();
/*GEThomepage.*/
router.get('/',function(req,res,next){
res.render('index',{title:'Express',name:'World'});
});
module.exports=router;
Ifwerefreshourbrowseragainatthispoint,westillwon'tseethename.That'sbecauseourapplicationhasalreadyloadedourroute,sowon'tpickupthechange.
Gobacktoyourterminalandkilltherunningapplication.Startitagain(usingnpmstart)andreloadthepageinthebrowser.YoushouldnowseethetextHello,World!.
UsingnodemonforautomaticrestartsRestartingtheapplicationeverytimewemakeachangeisabittedious.Wecandobetterbyrunningourapplicationwithnodemon,whichwillautomaticallyrestarttheapplicationwheneverwemakeachange:
>npminstall-gnodemon
>nodemon
Tryupdatingtheroutes/index.jsfileagain(forexample,changethenamestringtoyourownname),thenrefreshthebrowser.Thistime,thechangeshouldappearwithoutyouneedingtomanuallystopandrestarttheapplication.Notethattheprocessisrestartedbynodemonthough,soifourapplicationstoredanyinternalstate,thiswouldbelost.
CreatingmodularapplicationswithExpressTofindouthowourroutegetscalledwhenarequestismade,weneedtolookattheapp.jsbootstrappingfile.Seethefollowingtwolines:
varroutes=require('./routes/index');
...
app.use('/',routes);
ThistellsExpresstousetheroutingcontextdefinedinroutes/index.jsforrequeststotherootpath('/').
Thereisasimilarcallsettinguparouteunderthe/userspath.Tryvisitingthispathinyourbrowser.Theroutethatrendersthisresponseisdefinedin/routes/users.js.
Notethattheroutein/routes/users.jsisalsoboundto'/',thesameastheroutein/routes/index.js.ThereasonthisworksisthatthesepathsareeachrelativetoaseparateRouterinstance,andtheinstancecreatedin/routes/users.jsismountedunderthe/userspathinapp.js.
Thismechanismmakesiteasytobuildlargeapplicationscomposedfromsmallermodules.YoucanthinkofitassimilartotheAreasfunctionalityinASP.NETMVC,orsimplyasanalternativestructuretoMVCcontrollersgroupingtogetheractionmethods.
BootstrappinganExpressapplicationLet'stakealookattherestoftheapp.jsfile.YourfilemightnotlookidenticaltothelistingsbelowduetominordifferencesinourversionsofExpress,butitwillcontainbroadlythesamesections.
Thevariousrequire()callsatthetopofthefileimportthemodulesusedbytheapplication,includingbuilt-inNode.jsmodules(HTTPandPath),third-partylibraries,andtheapplication'sownroutes.ThefollowinglinesinitializeExpress,tellingitwheretolookforviewtemplatesandwhatrenderingenginetouse(inourcase,Hogan):
varapp=express();
//viewenginesetup
app.set('views',path.join(__dirname,'views'));
app.set('viewengine','{views}');
Therestofthefileconsistsofcallstoapp.use().Theseregistervariousdifferentmiddlewareforprocessingtherequest.Theorderinwhichtheyareregisteredformsarequestprocessingpipeline.YoumightalreadybefamiliarwiththispatternfromservletfiltersinJava,ortheIAppBuilder/IApplicationBuilder/IBuilderinterfacesinOWINandASP.NET.Don'tworryifnotthough;we'llexploremiddlewarethoroughlyhere.
UnderstandingExpressmiddlewareMiddlewarefunctionsarethefundamentalbuildingblocksofanExpressapplication.Theyaresimplyfunctionsthattakerequestandresponsearguments(justlikeourlistenerfunctionsbefore)andareferencetothenextmiddlewareinthechain.
Eachmiddlewarefunctioncanmanipulatetherequestandresponseobjectsbeforepassingontothenextmiddlewareinthechain.Bychainingmiddlewaretogetherinthisway,youcanbuildcomplexfunctionalityfromsimplemodularcomponents.Italsoallowscleanseparationbetweenyourapplicationlogicandcross-cuttingconcernssuchaslogging,authentication,orerrorhandling.
Insteadofpassingcontroltothenextmiddlewareinthechain,afunctioncanalsoendtheprocessingoftherequestandreturnaresponse.Middlewarecanalsobemountedtospecificpathsorrouterinstances,forexample,ifwewantenhancedloggingonaparticularpartofoursite.
Infact,Expressroutesarejustanotherexampleofmiddleware:theroutesthatwehavealreadylookedatareordinarymiddlewarefunctionswiththesamethreeargumentsnotedabove.Theyjusthappentobemountedtoaspecificpathandtoreturnaresponse.
Implementingerrorhandling
Let'stakeacloserlookatsomeofthemiddlewareinapp.js.First,lookatthe404errorhandler:
app.use(function(req,res,next){
varerr=newError('NotFound');
err.status=404;
next(err);
});
Thisfunctionalwaysreturnsaresponse.Sowhydowenotalwaysgeta404fromourapplication?Rememberthatmiddlewareiscalledinorder,andtheroutes(whichareregisteredbeforethisfunction)returnaresponseanddon'tcallthenextmiddleware.Thismeansthatthe404functionwillonlybecalledforrequeststhatdon'tmatchanyroute,whichisexactlywhatwewant.
Whatabouttheothertwoerrorhandlersinapp.js?Theyreturna500responsewithacustomerrorpage.Whydoesourapplicationnotreturna500responseinallcases?Howdothesegetexecutedifanothermiddlewarethrowsanerrorbeforecallingnext()?
Error-handlingisaspecialcaseinExpress.Error-handlingmiddlewarefunctionstakefourargumentsinsteadofthree,withthefirstparameterbeinganerror.Theyshouldberegisteredlast,afterallothermiddlewares.
Inthecaseofanerror(eitheranerrorbeingthrownoramiddlewarefunctionpassinginanerrorargumentwhencallingnext),Expresswillskipanyothernon-errorhandlingmiddlewareand
startexecutingtheerrorhandlers.
UsingExpressmiddleware
Let'sseesomeExpressmiddlewareinactionbymakinguseofcookieparsingmiddleware(whichisalreadypartoftheskeletonapplicationcreatedbyexpress-generator).Wecandothisbyusingacookietostorehowmanytimessomeonehasvisitedthesite.Updateroutes/index.jsasfollows:
router.get('/',function(req,res,next){
varvisits=parseInt(req.cookies.visits)||0;
visits+=1;
res.cookie('visits',visits);
res.render('index',
{title:'Express',name:'World',visits:visits}
);
});
Andaddanewlinetoviews/index.hjs:
<p>Youhavevisitedthissite{{visits}}time(s).</p>
Nowvisithttp://localhost:3000/againandrefreshthepageafewtimes.Youshouldseethevisitcountincreasebasedonthevaluestoredinthecookie.Toseewhatthecookieparsingmiddlewareisdoingforus,trydeletingorcommentingoutthefollowinglinefromapp.jsandreloadingthepage:
app.use(cookieParser());
Asyoucanseefromtheerror,thecookiespropertyoftherequestisnowundefined.ThecookieparsingmiddlewarelooksatthecookieheaderoftherequestandturnsitintoaconvenientJavaScriptobjectforus.Thisisacommonusecaseformiddleware.ThebodyParsermiddlewarefunctionsdoaverysimilarjobwiththerequestbody,turningrawtextintoaJavaScriptobjectthatiseasiertouseinourroutes.
Notethattheerrorresponseabovealsodemonstratesourerrorhandlingmiddleware.Trycommentingouttheerrorhandlersattheendoftheapp.jsfileandreloadingthepageagain.Wenowgetthedefaultstacktraceratherthanthecustomerrorresponsedefinedinourhandler.
SummaryInthischapter,weinstalledNode.js,sawhowtointeractwithitfromthecommandline,andstartedusingittowritewebapplications.WelearnedaboutExpressandhowwecanstructureanapplicationusingroutesandmiddleware.
Althoughwe'veseensomecodeinthischapter,wehaven'treallyexploredtheJavaScriptsyntaxindetail.Beforeaddingmorefunctionalitytoourapplication,weshouldmakesurethatwe'reuptospeedwithJavaScript.Thisisthesubjectofthenextchapter.
Chapter3.AJavaScriptPrimerIt'simportanttohaveasolidunderstandingofJavaScripttowriteNode.jsapplications.JavaScriptisnotalargeorcomplexlanguage,butitmayseemunusual,andhasafewquirksandgotchastowatchoutfor.
TherecentreleaseofECMAScript2015(previouslynamedES6)introducesanumberofnewlanguagefeaturestomakeJavaScriptprogrammingeasierandsafer.NotallES2015featuresareavailableinallimplementationsyet.However,alltheES2015featureswe'llmentioninthischapterareavailableinNode.jsandinmostotherJavaScriptenvironments.
Inthischapter,we'llfamiliarizeourselveswithJavaScriptsowecanwriteNode.jsapplicationswithconfidence.Wewillcoverthefollowingtopics:
TheJavaScripttypesystemJavaScriptasafunctionalprogramminglanguageObject-orientedprogramminginJavaScriptJavaScript'sprototype-basedinheritance
IntroducingJavaScripttypesJavaScriptisadynamically-typedlanguage.Thesemeansthattypesarecheckedatruntimewhenyoutrytodosomethingwithavariable,ratherthanbyacompiler.Forexample,thefollowingisvalidJavaScriptcode:
varmyVariable=0;
console.log(typeofmyVariable);//Prints"number"
myVariable="1";
console.log(typeofmyVariable);//Prints"string"
Althoughvariablesdohaveatype,thismaychangethroughoutthelifetimeofthevariable.
JavaScriptalsotriestoimplicitlyconverttypeswherepossible,forexample,usingtheequalityoperator:
console.log(2=="2");//Prints"true"
AlthoughthismightmakesenseforfrontendJavaScript(forexamplecomparingagainstthevalueofaforminput),ingeneral,itismorelikelytobeasourceoferrorsorconfusion.Forthisreason,itisrecommendedtoalwaysusethestrictequalityandinequalityoperators:
console.log(2==="2");//Prints"false"
console.log(2!=="2");//Prints"true"
JavaScriptprimitivetypesJavaScripthasasmallnumberofprimitivetypes,similartoC#andJava.Thesearestring,number,andBoolean,aswellasthespecialsingle-valuedtypes,nullandundefined.ES2015alsoaddsthesymboltype,butwewon'tcoverithereasitsusecasesaremoreadvanced.
Stringsareimmutable,likeinC#andJava.Concatenatingstringscreatesanewstringinstance.Stringliteralscanbedefinedwithdoublequotes(asinC#orJava)orsinglequotes.Thesecanbeusedinterchangeably(usuallywhateveriseasiertoavoidescaping).
ES2015alsointroducessupportfortemplatestrings,whicharedefinedusingbackticksandcanincludeinterpolatedexpressions.
Hereareseveralwaystodefinethesamestring:
varsingleQuoted='"Hey",Isaid,"I\'mastring"';
vardoubleQuoted="\"Hey\",Isaid,\"I'mastring\"";
console.log(doubleQuoted===singleQuoted);//Prints"true"
varexpression='Hey';
vartemplated=`"${expression}",Isaid,"I'mastring"`;
console.log(templated===singleQuoted);//Prints"true"
NumberisJavaScript'sonlybuilt-innumerictype.Itisadouble-precision64-bitfloating-pointnumber,likedoubleinC#orJava.IthasspecialvaluesNaN(notanumber)andInfinityforvaluesthatcannotberepresentedotherwise:
console.log(1/0);//Prints"Infinity"
console.log(Infinity+1);//Prints"Infinity"
console.log((1/0)===(2/0));//Prints"true"
varnotANumber=parseInt("foo");
console.log(notANumber);//Prints"NaN"
console.log(notANumber===NaN);//Prints"false"
console.log(isNaN(notANumber));//Prints"true"
Note
NotethatalthoughthereisonlyasingleNaNvalue,itisnottreatedasequaltoitself.JavaScriptprovidesthespecialisNaNfunctionfortestingwhetheravariablecontainstheNaNvalue.
Thenulltypehasasingleinstance,representedbytheliteralnull,justasinC#orJava.JavaScriptalsohastheundefinedtype.Variablesorparametersthathaveneverbeenassignedwillhavethevalueundefined:
vardeclared;
console.log(typeofdeclared);//Prints"undefined"
console.log(declared===undefined);//Prints"true"
console.log(typeofundeclared);//Prints"undefined"
console.log(undeclared===undefined);//throwsReferenceError
Notethatourundeclaredidentifiercannotbeaccessedasavariableinnormalcodebecauseithasnotbeendeclared.However,wecanpassittothetypeofoperator,whichevaluatestotheundefinedtype.
Functionalobject-orientedprogrammingJavaScriptisafunctionalobject-orientedprogramminglanguage.However,itisquitedifferenttootherobject-orientedprogramminglanguagessuchasC#orJava.Despitehavingasimilarsyntax,therearesomeimportantdifferences.
FunctionalprogramminginJavaScriptInJavaScript,functionsarefirst-classobjects.Thismeansthatfunctionscanbetreatedlikeanyotherobject:theycanbecreateddynamically,assignedtovariables,orpassedintomethodsasarguments.
Thismakesitveryeasytospecifyeventcallbacks,ortoprograminamorefunctionalstyleusinghigher-orderfunctions.Higher-orderfunctionsarefunctionsthattakeotherfunctionsasarguments,and/orreturnanotherfunction.Here'satrivialexampleoffilteringanarrayofnumbersfirstinanimperativestyleandtheninafunctionalstyle.NotethatthisexamplealsoshowsJavaScript'sarrayliteralnotationforcreatingarrays,usingsquarebrackets.ItalsodemonstratesJavaScript'sconditionalconstructandoneofitsloopconstructs,whichshouldbefamiliarfromotherlanguages:
varnumbers=[1,2,3,4,5,6,7,8];
varfilteredImperatively=[];
for(vari=0;i<numbers.length;++i){
varnumber=numbers[i];
if(number%2===0){
filteredImperatively.push(number);
}
}
console.log(filteredImperatively);//Prints[2,4,6,8]
varfilteredFunctionally=
numbers.filter(function(x){returnx%2===0;});
console.log(filteredFunctionally);//Prints[2,4,6,8]
Thesecondapproachintheexamplemakesuseofafunctionexpressiontodefineanew,anonymousfunctioninline.Ingeneral,thisisreferredtoasalambdaexpression(afterlambdacalculusinmathematics).Thisfunctionispassed-intothebuiltinfilterexpressionavailableonJavaScriptarrays.
InC#,assignmentandpassingofbehaviorwasoriginallyonlypossibleusingdelegates.SinceC#3.0,supportforlambdaexpressionsmakesitmucheasiertousefunctionsinthisway.Thisallowsamorefunctionalstyleofprogramming,forexample,usingC#'sLanguage-IntegratedQuery(LINQ)features.
InJava,foralongtimetherewasnonativewayforafunctiontoexistindependently.Youwouldhavetodefineamethodona(possiblyanonymous)classandpassthisaround,addingalotofboilerplate.Java8introducessupportforlambdaexpressionsinasimilarwaytoC#.
WhileC#andJavamayhavetakenawhiletocatchup,youmightbethinkingthatJavaScriptisnowfallingbehind.ThesyntaxfordefininganewfunctioninJavaScriptisquiteclumsycomparedtothelambdasyntaxinC#andJava.
ThisisespeciallyunfortunatesinceJavaScriptusesaC-likesyntaxforfamiliaritywithother
languageslikeJava!ThisisresolvedinES2015witharrowfunctions,allowingustorewritethepreviousexampleasfollows:
varnumbers=[1,2,3,4,5,6,7,8];
varfilteredFunctionally=numbers.filter(x=>x%2===0);
console.log(filteredFunctionally);//Prints[2,4,6,8]
Thisisasimplearrowfunctionwithasingleargumentandasingleexpression.Inthiscase,theexpressionisimplicitlyreturned.
Note
Itcanbeusefultoreadthe=>notationinarrowfunctionsasgoesto.
Arrowfunctionsmayhavemultiple(orzero)arguments,inwhichcasetheymustbesurroundedbyparentheses.Ifthefunctionbodyisenclosedinbraces,itmaycontainmultiplestatements,inwhichcasethereisnoimplicitreturn.TheseareexactlythesamesyntaxrulesasforlambdaexpressionsinC#.
Hereisamorecomplexarrowfunctionexpressionthatreturnsthemaximumofitstwoarguments:
varmax=(a,b)=>{
if(a>b){
returna;
}else{
returnb;
}
};
UnderstandingscopesinJavaScriptTraditionally,inJavaScript,thereareonlytwopossiblevariablescopes:globalandfunctional.Thatis,anidentifier(avariablename)isdefinedglobally,orforanentirefunction.Thiscanleadtosomesurprisingbehavior,forexample:
functionscopeDemo(){
for(vari=0;i<10;++i){
varj=i*2;
}
console.log(i,j);
}
scopeDemo();
Inmostotherlanguages,youwouldexpectitoexistforthedurationoftheforloop,andjtoexistforeachloopiteration.Youwouldthereforeexpectthisfunctiontologundefinedundefined.Infact,itlogs1018.Thisisbecausethevariablesarenotscopedtotheblockoftheforloop,buttotheentirefunction.Sotheprecedingcodeisequivalenttothefollowing:
functionscopeDemo(){
vari,j;
for(i=0;i<10;++i){
j=i*2;
}
console.log(i,j);
}
scopeDemo();
JavaScripttreatsallvariabledeclarationsasiftheyweremadeatthetopofthefunction.Thisisknownasvariablehoisting.Althoughconsistent,thiscanbeconfusingandleadtosubtlebugs.
ES2015introducestheletkeywordfordeclaringvariables.Thisworksexactlythesameasvarexceptthatvariablesareblock-scoped.Thereisalsotheconstkeyword,whichworksthesameasletexceptthatitdoesnotallowreassignment.Itisrecommendedthatyoualwaysuseletratherthanvar,anduseconstwhereverpossible.Checkthefollowingcodeforexample:
functionscopeDemo(){
"usestrict";
for(leti=0;i<10;++i){
letj=i*2;
}
console.log(i,j);//ThrowsReferenceError:iisnotdefined
}
scopeDemo();
Notethe"usestrict"stringintheprecedingexample.We'lldiscussthisinthenextsection.
Strictmode
The"usestrict"stringisahinttotheJavaScriptinterpretertoenableStrictMode.Thismakesthelanguagesaferbytreatingcertainusagesofthelanguageaserrors.Forexample,
mistypingavariablenamewithoutstrictmodewilldefineanewvariableatthegloballevel,ratherthancausinganerror.
StrictmodeisalsonowusedbysomebrowserstoenablefeaturesinthenewestversionofJavaScript,suchastheletandconstkeywordspreviouslyshown.Ifyouarerunningtheseexamplesinabrowser,youmayfindthattheprecedinglistingdoesn'tworkwithoutstrictmode.
Inanycase,youshouldalwaysenablestrictmodeinallofyourproductioncode.The"usestrict"stringaffectsallcodeinthecurrentscope(thatis,JavaScript'straditionalfunctionalorglobalscope),soshouldusuallybeplacedatthetopofafunction(orthetopofamodule'sscriptfileinNode.js).
Object-orientedprogramminginJavaScriptAnythingthatisnotoneofJavaScript'sbuilt-inprimitives(strings,number,null,andsoon)isanobject.Thisincludesfunctions,aswe'veseenintheprevioussection.Functionsarejustaspecialtypeofobjectthatcanbeinvokedwitharguments.Arraysareaspecialtypeofobjectwithlist-likebehavior.Allobjects(includingthesetwospecialtypes)canhaveproperties,whicharejustnameswithavalue.YoucanthinkofJavaScriptobjectsasadictionarywithstringkeysandobjectvalues.
Objectscanbecreatedwithpropertiesusingtheobjectliteralnotation,asinthefollowingexample:
varmyObject={
myProperty:"myValue",
myMethod:function(){
return`myPropertyhasvalue"${this.myProperty}"`;
}
};
console.log(myObject.myMethod());
Youmightfindthisnotationfamiliarevenifyou'veneverwrittenanyJavaScript,asitisthebasisforJSON.Notethatamethodisjustanobjectpropertythathappenstohaveafunctionasitsvalue.Alsonotethatwithinmethods,wecanrefertothecontainingobjectusingthethiskeyword.
Finally,notethatwedidnotneedtodefineaclassforourobject.JavaScriptisunusualamongstobject-orientedlanguagesinthatitdoesn'treallyhaveclasses.
Programmingwithoutclasses
Inmostobject-orientedlanguages,wecandeclaremethodsinaclassforusebyallofitsobjectinstances.Wecanalsosharebehaviorbetweenclassesthroughinheritance.
Let'ssaywehaveagraphwithaverylargenumberofpoints.Thesemayberepresentedbyobjectsthatarecreateddynamicallyandhavesomecommonbehavior.Wecouldimplementpointslikethis:
functioncreatePoint(x,y){
return{
x:x,
y:y,
isAboveDiagonal:function(){
returnthis.y>this.x;
}
};
}
varmyPoint=createPoint(1,2);
console.log(myPoint.isAboveDiagonal());//Prints"true"
OneproblemwiththisapproachisthattheisAboveDiagonalmethodisredefinedforeachpointonourgraph,thustakingupmorespaceinmemory.
Wecanaddressthisusingprototypalinheritance.AlthoughJavaScriptdoesn'thaveclasses,objectscaninheritfromotherobjects.Eachobjecthasaprototype.Ifwetrytoaccessapropertyonanobjectandthatpropertydoesn'texist,theinterpreterwilllookforapropertywiththesamenameontheobject'sprototypeinstead.Ifitdoesn'texistthere,itwillchecktheprototype'sprototype,andsoon.Theprototypechainwillendwiththebuilt-inObject.prototype.
Wecanimplementthisforourpointobjectsasfollows:
varpointPrototype={
isAboveDiagonal:function(){
returnthis.y>this.x;
}
};
functioncreatePoint(x,y){
varnewPoint=Object.create(pointPrototype);
newPoint.x=x;
newPoint.y=y;
returnnewPoint;
}
varmyPoint=createPoint(1,2);
console.log(myPoint.isAboveDiagonal());//Prints"true"
TheisAboveDiagonalmethodnowonlyexistsonceinmemory,onthepointPrototypeobject.
WhenwetrytocallisAboveDiagonalonanindividualpointobject,itisnotpresent,butitisfoundontheprototypeinstead.
Notethatthistellsussomethingimportantaboutthethiskeyword.Itactuallyreferstotheobjectthatthecurrentfunctionwascalledon,ratherthantheobjectitwasdefinedon.
Creatingobjectswiththenewkeyword
Wecanrewritetheprecedingcodeexampleinaslightlydifferentform,asfollows:
varpointPrototype={
isAboveDiagonal:function(){
returnthis.y>this.x;
}
}
functionPoint(x,y){
this.x=x;
this.y=y;
}
functioncreatePoint(x,y){
varnewPoint=Object.create(pointPrototype);
Point.apply(newPoint,arguments);
returnnewPoint;
}
varmyPoint=createPoint(1,2);
Thismakesuseofthespecialargumentsobject,whichcontainsanarrayoftheargumentstothecurrentfunction.Italsousestheapplymethod(whichisavailableonallfunctions)tocallthePointfunctiononthenewPointobjectwiththesamearguments.
Atthemoment,ourpointPrototypeobjectisn'tparticularlycloselyassociatedwiththePointfunction.Let'sresolvethisbyusingthePointfunction'sprototypepropertyinstead.Thisisabuilt-inpropertyavailableonallfunctionsbydefault.Itjustcontainsanemptyobjecttowhichwecanaddadditionalproperties:
functionPoint(x,y){
this.x=x;
this.y=y;
}
Point.prototype.isAboveDiagonal=function(){
returnthis.y>this.x;
}
functioncreatePoint(x,y){
varnewPoint=Object.create(Point.prototype);
Point.apply(newPoint,arguments);
returnnewPoint;
}
varmyPoint=createPoint(1,2);
Thismightseemlikeaneedlesslycomplicatedwayofdoingthings.However,JavaScripthasaspecialoperatorthatallowsustogreatlysimplifythepreviouscode,asfollows:
functionPoint(x,y){
this.x=x;
this.y=y;
}
Point.prototype.isAboveDiagonal=function(){
returnthis.y>this.x;
}
varmyPoint=newPoint(1,2);
ThebehaviorofthenewoperatorisidenticaltoourcreatePointfunctioninthepreviousexample.Thereisonesmallexception:ifthePointfunctionactuallyreturnedavalue,thenthiswouldbeusedinsteadofnewPoint.ItisconventionalinJavaScripttostartfunctionswithacapitalletteriftheyareintendedtobeusedwiththenewoperator.
Programmingwithclasses
AlthoughJavaScriptdoesn'treallyhaveclasses,ES2015introducesanewclasskeyword.Thismakesitpossibletoimplementsharedbehaviorandinheritanceinawaythatmaybemorefamiliarcomparedtootherobject-orientedlanguages.
Theequivalentofourprecedingcodewouldlooklikethefollowing:
classPoint{
constructor(x,y){
this.x=x;
this.y=y;
}
isAboveDiagonal(){
returnthis.y>this.x;
}
}
varmyPoint=newPoint(1,2);
Notethatthisreallyisequivalenttoourprecedingcode.Theclasskeywordisjustsyntacticsugarforsettinguptheprototype-basedinheritancealreadydiscussed.
Class-basedinheritance
Asmentionedbefore,anobject'sprototypemayinturnhaveanotherprototype,allowingachainofinheritance.Settingupsuchachainbecomesquitecomplicatedusingtheprototype-basedapproachfromtheprevioussection.Itismuchmoreintuitiveusingtheclasskeyword,asinthefollowingexample(whichmightbeusedforplottingagraphwitherrorbars):
classUncertainPointextendsPoint{
constructor(x,y,uncertainty){
super(x,y);
this.uncertainty=uncertainty;
}
upperLimit(){
returnthis.y+this.uncertainty;
}
lowerLimit(){
returnthis.y-this.uncertainty;
}
}
varmyUncertainPoint=newPoint(1,2,0.5);
SummaryInthischapter,wehaveintroducedJavaScript'stypesystem,understoodfunctionsasfirst-classobjectsinJavaScript,seenhowJavaScriptdiffersfromotherobject-orientedlanguages,implementedinheritanceusingprototypesandclasses,andlearnedthenewfeaturesofECMAScript2015(ES6)thatmakethelanguagesaferandmoreintuitivetouse.
NowthatyouhaveafirmgroundinginJavaScript,youcanstartwritingNode.jsapplicationswithconfidence.Inthenextchapter,wewillexpandonourExpressprojectandseehowthemodulesysteminNode.jsallowsustostructureourcodebase.
Chapter4.IntroducingNode.jsModulesNowthatwe'reuptospeedwiththesyntaxoftheJavaScriptlanguage,wecanstartbuildingupourapplication.Todothis,weneedtoknowhowtostructureourapplicationtoallowittogrowinamaintainableway.
Inthischapter,wewillcoverthefollowingtopics:
StructuringJavaScriptcodewithmodulesDeclaringandusingourownmodulesOrganizingmodulesintofilesanddirectoriesImplementinganExpressmiddlewaremodule
OrganizingyourcodebaseMostprogrammingplatformsprovideseveralmechanismsforstructuringyourcode.ConsiderC#/.NETorJava:youcanuseclasses,namespacesorpackages,andcompilationunits(assembliesorJAR/WARfiles).Noticetherangefromsmall-scaleorganizationalunits(classes)tolarge-scaleones(assemblies).Thisallowsyoutomakeacodebasemoreapproachablebyprovidingorderateachlevelofdetail.
Classicbrowser-basedJavaScriptdevelopmentwasquiteunstructured.Functionsweretheonlybuilt-inlanguagefeaturefororganizingyourcode.Youcouldsplityourcodeintoseparatescriptfiles,buttheseallsharethesameglobalcontextwithinawebpage.
Overtime,peoplehavedevelopedwaysoforganizingJavaScriptcode.Thestandardapproachnowistousemodules.ThereareafewdifferentmodulesystemsavailableforJavaScript,buttheyallworkinasimilarway.Eachmodulesystemincludesthefollowingaspects:
AwayofdeclaringamodulewithanameanditsownscopeAwayofdefiningfunctionalityprovidedbythemoduleAwayofimportingamoduleintoanotherscript
Ineachsystem,whenyouimportamodule,yougetaplainJavaScriptobjectthatyoucanassigntoavariable.Formostmodules,thiswillbeanobjectwithseveralpropertiescontainingfunctions.ButitcouldbeanyvalidJavaScriptobject,forexample,asinglefunction.
Mostmodulesystemsexpectoratleastencourageyoutodefineeachmoduleinaseparatefile,justasyouwouldwithclassesinotherlanguages.Itisalsocommonforlargemodulestobecomposedofother,smaller,modules.Thesewouldbegroupedtogetherunderthesamedirectory.Inthisway,modulesactmorelikenamespacesorpackages.
Theflexibilityofmodulesmeansthatyoucanusethemtostructureyourcodeatdifferentscales.Thelackofabuilt-inhierarchyoforganizationalunitsinJavaScriptprovidesmoreflexibility.Italsoforcesyoutothinkmoreabouthowyoustructureyourcode.
JavaScriptmodulesystemsECMAScript2015introducesmodulesasabuilt-infeatureofthelanguage.Theyhavebeencommonpracticeforawhile,though.Forclient-sideprogramming,thispracticehasreliedonusingthird-partylibrariestoprovideamodulesystem.
YoumayhaveseenRequireJS,whichprovidesawayofusingfunctionstodefinemodules.RequireJSusesplainJavaScriptandworksinanyenvironment.Itismostusefulinthebrowser,whereadditionalmodulesmaybeloadedovertheInternet.RequireJSaddressessomeofthepitfallsofloadingadditionalscriptsdynamicallyandasynchronously.
TheNode.jsenvironmenthasitsownmodulesystem,whichwewilllookatintherestofthischapter.Itmakesuseofthefilesystemfororganizingmodules.
Tip
YoumightcomeacrossthetermsAMDorCommonJS.Thesearestandardsfordefiningmodules.RequireJSisanimplementationofAMD,andNode.jsmodulesfollowtheCommonJSstandard.ECMAScript2015modulesdefineanewstandardwithnewexportandimportlanguagekeywords.Thesyntaxisquitesimilar,though,totheNode.jsmodulesystemwe'llbeusinginthisbook,anditiseasytoswitchbetweenthetwo.
CreatingmodulesinNode.jsWe'veactuallyalreadyusedseveralNode.jsmodulesandcreatedsomeofourown.Let'slookagainatourapplicationfromChapter2,GettingStartedwithNode.js.
Thefollowingcodeisfromroutes/index.jsandroutes/users.js:
module.exports=router;
Thefollowingisthecodefromapp.js:
varexpress=require('express');
varpath=require('path');
varfavicon=require('serve-favicon');
varlogger=require('morgan');
varcookieParser=require('cookie-parser');
varbodyParser=require('body-parser');
varroutes=require('./routes/index');
varusers=require('./routes/users');
Eachofourroutes(indexandusers)isamodule.Theyexposetheirfunctionalityusingthebuilt-inmoduleobject,whichisdefinedbyNode.jsasavariablescopedtoeachmodule.Intheprecedingexample,theobjectprovidedbyeachofourroutemodulesisanExpressrouterinstance.Theapp.jsscriptimportsthesemodulesusingthebuilt-inrequirefunction.
Observethatapp.jsalsoimportsvariousnpmpackagesusingrequire.Notethatitusesfilepathstoreferenceourownmodules,whereasnpmmodulesarereferencedbyname.
Let'slookathowNode.jsmodulessatisfythethreeaspectsofJavaScriptmodulefunctionality.
DeclaringamodulewithanameanditsownscopeInNode.js,eachseparateJavaScriptfileisautomaticallytreatedasanewmodule.Unlikescriptsloadedintoawebpage,eachfilehasitsownscope.Thenameofthemoduleisthenameofthefile.
DefiningfunctionalityprovidedbythemoduleNode.jsprovidestwobuilt-invariablesforexportingfunctionalityfromamodule.Thesearemodule.exportsandexports.module.exportsisinitializedtoanemptyobject.exportsisjustareferencetomodule.exports.Itisequivalenttothefollowingappearingbeforeyourscript:
varexports=module.exports={};
Whateveriscontainedinthemodule.exportsvariableattheendofyourscriptistheexportedvalueofyourmodule.Thiswillbereturnedwheneveryourmoduleisimportedelsewhere.Thefollowingareallequivalent:
module.exports.foo=1;
module.exports.bar=2;
module.exports={foo:1,bar:2};
exports.foo=1;
exports.bar=2;
Notethatthefollowingisnotthesameasthepreviousexamples.Itjustreassignsexports,butdoesn'taltermodule.exportsatall:
exports={foo:1,bar:2};
ImportingamoduleintoanotherscriptNode.jsprovidesanotherbuilt-invariableforimportingmodules.Thisistherequirefunctionwesawinapp.jsearlierinthechapter.ThisfunctionisprovidedbyNode.jsandalwaysavailable.Ittakesasingleargument,whichisthenameorpathofthemoduleyouwanttoimport.Thefollowingexcerptsfromapp.jsdemonstrateloadingathird-partymodulebynameandoneofourownmodulesbyafilepath:
varexpress=require('express');
...
varroutes=require('./routes/index');
Notethatwedon'tneedtospecifythe.jsfileextensionforourownmodule.Node.jswillautomaticallyaddthisforus.
Definingadirectory-levelmoduleAsmentionedatthebeginningofthischapter,modulescanalsoactmorelikenamespaces.Wecantreatawholedirectoryasamodule,consistingofsmallermodulesinindividualfiles.Thesimplestwaytodothisistocreateanindex.jsfileinthedirectory.
Whencallingrequire('./directoryName'),Node.jswillattempttoloadafilenamed'./directoryName/index.js'(relativetothecurrentscript).Thereisnothingspecialaboutindex.jsitself.Thisisjustanotherscriptfilethatexposesanentrypointtothemodule.IfdirectoryNamecontainsapackage.jsonfile,Node.jswillloadthisfilefirstandseeifitspecifiesamainscript,inwhichcaseNode.jswillloadthisscriptinsteadoflookingforindex.js.
Toimportlocalmodules,weuseafileordirectorypath,thatis,somethingstartingwith'/','../',or'./'asintheprecedingexample.Ifwecallrequirewithaplainstring,Node.jstreatsitasrelativetothenode_modulesfolder.Thenpmpackagesarejustdirectory-levelmodulesunderthisfolder.Wewilllookatdefiningourownnpmpackagesinmoredetailinalaterchapter.
ImplementinganExpressmiddlewaremoduleLet'sreturntotheNode.jsapplicationwestartedinChapter2,GettingStartedwithNode.js.We'regoingtowriteanapplicationwhereuserscansetpuzzlesforoneanother.Firstofall,we'llneedawayofidentifyingthecurrentuser.We'llneedtodothisonmostrequests,makingitacross-cuttingconcern.Thisisagoodusecaseformiddleware.
Fornow,wewillimplementusersinthesimplestwaypossible,juststoringanIDinacookie.Wewilllookintomorerobustidentificationinalaterchapter.Note,however,thatouruseofmiddlewaremeansitwillbeeasytoalterourapproachlateron.Thisconcernisencapsulatedinourusermiddleware,soweonlyneedtochangeitinoneplace.
First,weneedawayofgeneratinguniqueIDs.Forthis,wewillusetheUUIDmodulefromnpm.Wecanaddthistoourprojectbyrunningthefollowingonthecommandline:
>npminstalluuid--save
The--saveflagstoresthenameofthismoduleinourpackage.jsonfilesothatitwillbeinstalledautomaticallybynpminstall.Thisisusefulforrestoringourapplicationfromacleancheckoutofthesourcecode(recallthatpeoplecommonlyexcludethenode_modulesdirectoryfromsourcecontrol,preciselybecauseitcaneasilyberestoredinthisway).
Nowwearereadytocreateourmiddleware,whichwillplaceundermiddleware/users.js:
'usestrict';
constuuid=require('uuid');
module.exports=function(req,res,next){
letuserId=req.cookies.userId;
if(!userId){
userId=uuid.v4();
res.cookie('userId',userId);
}
req.user={
id:userId
};
next();
};
NoticethatweusetheES2015constkeywordfortheuuidmodulebecausethisreferenceneverchanges.ButweusetheletkeywordfortheuserIdvariablebecausethiscanbereassigned.Alsonoticethatwecallnext()ratherthanreturningaresponse,sothenextmiddlewarecancontinueprocessingtherequest.
Finally,weneedtoaddthismiddlewaretoourapplicationinapp.js:
varusers=require('./middleware/users');
varroutes=require('./routes/index');
varapp=express();
...
app.use(users);
app.use('/',routes);
...
Notethatthisreplacestheimportandusageofthe./routes/usersmodulethatwasgeneratedforus.Thisroutewasn'tparticularlyuseful,butwewilladdmoreroutessoon.
Wecancheckthatourmiddlewareworksbyalteringourindexrouteandviewasfollows:
routes/index.jsrouter.get('/',function(req,res,next){
res.render('index',{title:'Welcome',userId:req.user.id});
});
Thefollowingisthecodeviews/index.hjs:
<body>
<h1>{{title}}</h1>
<p>YouruserIDis{{userId}}.</p>
</body>
Launchtheapplicationandvisithttp://localhost:3000/.Youshouldseearandomly-generateduserID.RefreshthepageandyoushouldretainthesameID.Openthesiteinadifferentbrowser(oranincognito/privatebrowsingwindow).ThisseparatebrowsersessionshouldseeadifferentID.
SummaryInthischapter,wehaveseenhowtouseNode.jsmodulestostructureourcodebase,andhowtocreateanExpressmiddlewaremoduletoimplementcross-cuttingconcerns.
Nowthatwehaveawayofstructuringourcodebaseandameansofidentifyingusers,wecangetonwithimplementingourapplication'sfunctionality.Inthenextchapter,we'llstartaddingsomeinteractivitytoourapplication.
Chapter5.CreatingDynamicWebsitesNowthatwehaveestablishedabasicstructureforourapplication,wecanstarttoaddmorefunctionalityandbuildadynamicwebsitethatrespondstouserinput.
Inthischapter,wewillcoverthefollowingtopics:
AddinganewmoduletoourapplicationforstoringanddeletingdataExposingaJSONAPItohandleuser-submitteddataImplementingcommunicationbetweentheclientandserverusingAjaxBuildingupmorecomplexHTMLviewsusingpartialtemplates
Handlinguser-submitteddataWe'regoingtoimplementtheclassicguessinggameofHangman(seehttps://en.wikipedia.org/wiki/Hangman_(game)).Userswillbeabletopostnewwordstoguess,andtoguesswordspostedbyothers.We'lllookatcreatingnewgamesfirst.
First,we'lladdanewmoduleformanagingourgames.Fornow,we'lljuststoreourgamesinthememory.Ifwewanttoputgamesinsomepersistentstorageinfuture,thisisthemodulewewillchange.Theinterface(thatis,thefunctionsaddedtomodule.exports)canremainthesamethough.
Weaddthefollowingcodeunderservices/games.js:
'usestrict';
constgames=[];
letnextId=1;
classGame{
constructor(id,setBy,word){
this.id=id;
this.setBy=setBy;
this.word=word.toUpperCase();
}
}
module.exports.create=(userId,word)=>{
constnewGame=newGame(nextId++,userId,word);
games.push(newGame);
returnnewGame;
}
module.exports.get=
(id)=>games.find(game=>game.id===parseInt(id,10));
Nowlet'sgothroughourapplicationfromthetopdown.Inourindexview(views/index.hjs),we'lladdsimpleaHTMLformforcreatinganewgame.
<body>
<h1>{{title}}</h1>
<formaction="/games"method="POST">
<inputtype="text"name="word"
placeholder="Enterawordtoguess..."/>
<inputtype="submit"/>
</form>
<body>
Whensubmitted,thisformwillmakeaPOSTrequestto/games.Atthemoment,thiswouldreturna404errorsincewehavenothingmountedatthatroute(youcantrythisinabrowseritifyoulike).Wecanaddanewgamesroutetohandlethisrequest.Weaddthefollowingcodeunderroutes/games.js:
'usestrict';
constexpress=require('express');
constrouter=express.Router();
constservice=require('../services/games');
router.post('/',function(req,res,next){
constword=req.body.word;
if(word&&/^[A-Za-z]{3,}$/.test(word)){
service.create(req.user.id,word);
res.redirect('/');
}else{
res.status(400).send('Wordmustbeatleastthreecharacterslongand
containonlyletters');
}
});
module.exports=router;
Thereisquitealotgoingoninournewroutingmiddleware:
router.postcreatesahandlerforanHTTPPOSTrequest.req.bodycontainsformvalues,thankstothebodyParsermiddlewareinapp.js.req.user.idcontainsthecurrentuser,thankstoourusersmiddleware.res.redirect()issuesaredirecttoreloadthepage.ItisimportanttoalwaysissuearedirectafterasuccessfulPOSTrequest.Thisavoidsduplicateformsubmissions.res.status()setsanalternativeHTTPstatuscodefortheresponse,inthiscasea400foravalidationfailure.
Ourroutelooksforafieldnamedwordintherequestbody.Itthenchecksthisfieldisdefinedandnotempty(bothundefinedandtheemptystringarefalseyinJavaScript,sotheybehaveasfalseinconditionaltests).Italsochecksthatthefieldmatchesaregularexpressionspecifyingourvalidityrule.
Finally,theroutemakesuseofourservicemoduletoactuallycreatethenewgame.Itiscommonpracticeforroutingmiddlewaretodelegateapplicationlogictoothermodules.ItsmainresponsibilityistodefinetheHTTPinterfaceoftheapplication.Othermodulesareresponsibleforimplementingtheactualapplicationlogic.Inthisway,ourroutesandmiddlewarearecomparabletocontrollersinMVCframeworks.
Wealsoneedtomountthisrouteatthe/gamespath.Thefollowingcodeisfromapp.js:
varroutes=require('./routes/index');
vargames=require('./routes/games');
...
app.use('/',routes);
app.use('/games',games);
CommunicatingviaAjaxHavingcreatedagame,weneedawayofplayingit.Sincethewholepointofaguessinggameisthatthewordissecret,wedon'twanttosendthewholewordtotheclient.Instead,wejustwanttoletclientsknowthelengthofthewordandprovideawayforthemtoverifytheirguesses.
Todothis,we'llfirstneedtoexpandourgamesservicemodule:
classGame{
constructor(id,setBy,word){
this.id=id;
this.setBy=setBy;
this.word=word.toUpperCase();
}
positionsOf(character){
letpositions=[];
for(letiinthis.word){
if(this.word[i]===character.toUpperCase()){
positions.push(i);
}
}
returnpositions;
}
}
Nowwecanaddtwonewroutestoourgamesroute:
constcheckGameExists=function(id,res,callback){
constgame=service.get(id);
if(game){
callback(game);
}else{
res.status(404).send('Non-existentgameID');
}
}
router.get('/:id',function(req,res,next){
checkGameExists(
req.params.id,
res,
game=>res.render('game',{
length:game.word.length,
id:game.id
}));
});
router.post('/:id/guesses',function(req,res,next){
checkGameExists(
req.params.id,
res,
game=>{
res.send({
positions:game.positionsOf(req.body.letter)
});
}
);
});
Thesetworoutesmakeuseofasharedfunctionforretrievingthegameandreturninga404statuscodeifitdoesnotexist.TheGEThandlerrendersaview,aswithourindexroute.ThePOSThandlercallsres.send(),passinginaJavaScriptobject.ExpresswillautomaticallyturnthisintoaJSONresponsetotheclient.ThismakesitveryeasytobuildJSON-basedAPIsinexpress.
We'llnowcreateaviewandclient-sidescriptforcommunicatingwiththisAPI.Weaddthefollowingcodeunderviews/game.hjs:
<!DOCTYPEhtml>
<html>
<head>
<title>Hangman-Game#{{id}}</title>
<linkrel="stylesheet"href="/stylesheets/style.css"/>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.3/jquery.min.js">
</script>
<scriptsrc="/scripts/game.js"></script>
<basehref="/games/{{id}}/">
</head>
<body>
<h1>Hangman-Game#{{id}}</h1>
<h2id="word"data-length="{{length}}"></h2>
<p>Pressletterkeystoguess</p>
<h3>Missedletters:</h3>
<pid="missedLetters"></p>
</body>
</html>
Weaddthefollowingcodeunderpublic/scripts/game.js:
$(function(){
'usestrict';
varword=$('#word');
varlength=word.data('length');
//Createplaceholdersforeachletter
for(vari=0;i<length;++i){
word.append('<span>_</span>');
}
varguessedLetters=[];
varguessLetter=function(letter){
$.post('guesses',{letter:letter})
.done(function(data){
if(data.positions.length){
data.positions.forEach(function(position){
word.find('span').eq(position).text(letter);
});
}else{
$('#missedLetters')
.append('<span>'+letter+'</span>');
}
});
}
$(document).keydown(function(event){
//Letterkeyshavekeycodesintherange65-90
if(event.which>=65&&event.which<=90){
varletter=String.fromCharCode(event.which);
if(guessedLetters.indexOf(letter)===-1){
guessedLetters.push(letter);
guessLetter(letter);
}
}
});
});
Notethatintheclient-sidescriptwedropbacktotheECMAScript5standard(forexample,varinsteadoflet,andnoarrowfunction).Thisensuresthewidestpossiblecompatibility.ThelatestversionsofallmainstreambrowserswouldsupporttheelementsofES2015syntaxthatwe'vebeenusingsofarthough.
Alsonotethatwedon'thaveNode.jsmodulesavailableontheclientside.Wefallbacktowrappingourcodeinafunctiontoisolatethescope.We'lllookatwaystomakeclient-sidecodemoremodularinalaterchapter.
Ourclient-sidescriptusesjQuery.Wewon'tgointodetailonclient-sideframeworks,butit'sworthquicklyexplainingthefeaturesusedhere.ThejQuerylibraryprovidesaconsistentAPIforDOMmanipulationthatworksacrossallbrowsers,aswellasanumberofusefultoolsforclient-sidefunctionality.
ThemainjQueryAPIisavailablethroughthe$object,whichisafunction.Thefirstthingourscriptdoesiscall$andpassitacallback,whichjQuerywillexecuteoncethepagehasfinishedloading.Ourothercallsto$passinastringoraDOMelement.StringsareinterpretedasaCSSselectorforchoosingelements.Inbothcases,$returnsawrapperaroundasetofDOMelementswithsomeusefulmethods,forexample:
Thedatamethodallowsustoreadtheelements'data-attributesTheappendmethodallowsustoaddnewchildelementsMethodssuchaskeydownallowustobindhandlerfunctionsforevents
Therearealsosomeutilitymethodsdefinedonthe$objectitself.Thesearemorelikestaticmethodsanddon'trelatetoaspecificDOMelement.Thepost()methodisanexampleofthis.
OurscriptusesjQuery'spost()methodtoissueanAjaxPOSTrequest.Thisreturnsanobjectwithadone()method,towhichwecanpassacallbacktobeexecutedwhentherequestcompletes.Here,wecanmakeuseoftheJSONdatareturnedbyourAPI.Inthiscase,wefillin
anypositionsthatmatchourguessedletter.
Ifweruntheapplicationatthispoint,wehavea(very)minimalworkinggame.First,visithttp://localhost:3000/andcreateanewgamebysubmittingavalidword.Thenvisithttp://localhost:3000/games/1toplay.Itshouldlooksomethinglikethefollowing:
ImplementingotherdataoperationsSofar,wehaveseenhowtocreateorretrieveasinglegame,orsubmitasingleguessforagame.Applicationsalsocommonlyneedtolistdataordeleteentries.Theprinciplesherearemuchthesameaswe'veseenalready.Buttoimplementtheseoperations,we'llneedsomenewsyntax.
ListingdatainviewsLet'sfirstallowuserstoseealistofgamesthey'vecreatedorthathavebeencreatedbyothers.Ourchosenviewengine,Hogan,isbasedonMustache,whichhasaverysimplesyntaxfordisplayinglists.Wecanaddthesetwoliststoourindex.hjsview,asfollows:
<h2>Gamescreatedbyyou</h2>
<ulid="createdGames">
{{#createdGames}}
<li>{{word}}</li>
{{/createdGames}}
</ul>
<h2>Gamesavailabletoplay</h2>
<ulid="availableGames">
{{#availableGames}}
<li><ahref="/games/{{id}}">#{{id}}</a></li>
{{/availableGames}}
</ul>
Inordertopopulatetheselists,we'llneedacoupleofnewmethodsinourgames.jsservicemodule:
module.exports.createdBy=
(userId)=>games.filter(game=>game.setBy===userId);
module.exports.availableTo=
(userId)=>games.filter(game=>game.setBy!==userId);
Finally,we'llneedtoexposethesetoourindexviewfromourroute:
varexpress=require('express');
varrouter=express.Router();
vargames=require('../services/games');
router.get('/',function(req,res,next){
res.render('index',{
title:'Hangman',
userId:req.user.id,
createdGames:games.createdBy(req.user.id),
availableGames:games.availableTo(req.user.id)
});
});
module.exports=router;
Now,ourindexpageshowsgamescreatedbythecurrentuserandprovidesconvenientlinkstogamescreatedbyothers.Youcanexperimentwiththisfunctionalitybyusingtwoseparatebrowsersessionsagaintovisithttp://localhost:3000.Theresultshouldlooksomethinglikethefollowing:
IssuingadeleterequestfromtheclientToallowuserstoremovegamesthattheyhavecreated,we'llfirstneedtoaddamethodtoourGameclass:
classGame{
constructor(id,setBy,word){
this.id=id;
this.setBy=setBy;
this.word=word.toUpperCase();
}
positionsOf(character){
letpositions=[];
for(letiinthis.word){
if(this.word[i]===character.toUpperCase()){
positions.push(i);
}
}
returnpositions;
}
remove(){
games.splice(games.indexOf(this),1);
}
}
Nextwecancreateanewhandlerfordeleterequestsinourgamesroute:
router.delete('/:id',function(req,res,next){
checkGameExists(
req.params.id,
res,
game=>{
if(game.setBy===req.user.id){
game.remove();
res.send();
}else{
res.status(403).send(
'Youdon'thavepermissiontodeletethisgame'
);
}
}
);
});
Finally,wecanmakeuseofthisfromtheclient.Thefollowingcodeisfromviews/index.hjs:
<head>
<title>{{title}}</title>
<linkrel="stylesheet"href="/stylesheets/style.css"/>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.3/jquery.min.js">
</script>
<scriptsrc="/scripts/index.js"></script>
</head>
...
{{#createdGames}}
<liclass="game">
{{word}}
<aclass="delete"href="/games/{{id}}">(delete)</a>
</li>
{{/createdGames}}
Weaddthefollowingcodeunderpublic/scripts/index.js:
$(function(){
'usestrict';
$('#createdGames').on('click','.delete',function(){
var$this=$(this);
$.ajax($this.attr('href'),{
method:'delete'
}).done(function(){
$this.closest('.game').remove();
});
event.preventDefault();
});
});
Notethat,unlikeGETandPOST,jQueryhasnoconveniencefunctionfordeleterequests.Sowedropbacktothelowerlevel.ajax()functionandspecifytheHTTPmethodexplicitly.
Ifyouvisittheapplicationinabrowserandcreateanewgameagain,youshouldnowseealinktodeletethegame.
SplittingupExpressviewsusingpartialsDeletingagamedoesnotcausethepagetorefresh,butcreatinganewgamedoes.WecanfixthisbycreatinggamesviaanAjaxcall,consistentwithhowwedeletegames.Inorderforthistowork,theclient-sidescriptthathandlesthecallneedstoknowwhichHTMLtoaddtothepagewhenanewgameiscreated.
WecouldrepeattheHTMLstructureoftheviewwithintheclient-sideJavaScript.However,itwouldbebetterfortheservertoreturnthecorrectHTMLfragment,andtoreusethesametemplateforthisasitusesittorenderthelistonthepageinitially.
WecandothisbysplittingtheHTMLstructureforagamewithinthelistintoapartialview.ThisisaviewtemplateforanHTMLfragmentratherthanacompletepage.Weaddthefollowingcodeunderviews/createdGame.hjs:
<liclass="game">
{{word}}
<aclass="delete"href="/games/{{id}}">(delete)</a>
</li>
Withtheviewenginethatwe'reusing(Hogan),weneedtoletviewsknowaboutavailablepartialswhenrenderingthem(otherviewenginesallowpartialstoberesolvedautomatically).Thefollowingcodeisfromroutes/index.js:
res.render('index',{
title:'Hangman',
userId:req.user.id,
createdGames:games.createdBy(req.user.id),
availableGames:games.availableTo(req.user.id),
partials:{createdGame:'createdGame'}
});
Wecanusethepartialwithinourmainviewasfollows.We'llalsoaddIDstoourHTMLelements,whichwewillreferencefromourclient-sideJavaScriptshortly.Thefollowingcodeisfromviews/index.hjs:
<formaction="/games"method="POST"id="createGame">
<inputtype="text"name="word"id="word"
placeholder="Enterawordtoguess..."/>
<inputtype="submit"/></form>
<h2>Gamescreatedbyyou</h2>
<ulid="createdGames">
{{#createdGames}}
{{>createdGame}}
{{/createdGames}}
</ul>
Nowwecanupdateourgamesroutetoreturnonlythisfragmenttotheclientwhencreatinganewgame.Thefollowingcodeisfromroutes/games.js:
router.post('/',function(req,res,next){
letword=req.body.word;
if(word&&/^[A-Za-z]{3,}$/.test(word)){
constgame=service.create(req.user.id,word);
res.redirect(`/games/${game.id}/created`);
}else{
...
}
});
...
router.get('/:id/created',function(req,res,next){
checkGameExists(
req.params.id,
res,
game=>res.render('createdGame',game));
});
Finally,wecanmakeuseofthisinourclient-sidescript.Thefollowingcodeisfrompublic/scripts/index.js:
$(function(){
'usestrict';
$('#createGame').submit(function(event){
$.post($(this).attr('action'),{word:$('#word').val()},
function(result){
$('#createdGames').append(result);
});
event.preventDefault();
});
...
});
SummaryInthischapter,wehavestartedbuildingoutourownapplicationbycreatingnewmiddlewareandservicemodules.We'vereaduser-submitteddatafromformsandactedonit.We'veimplementedaJSONAPIontheserversideandcommunicatedwiththisfromtheclientusingAjax.We'veusedpartialviewstorendercommoncomponents.
Sofar,we'veseenhowtowriteJavaScriptcodeandimplementvariousfunctionalityinNode.js.Thisisgoodforprototyping,butisn'tenoughforamaintainableproject.It'salsoimportanttowriteautomatedtestsforourcode,whichisthesubjectofthenextchapter.
Chapter6.TestingNode.jsApplicationsSofar,wehaveonlybeentestingourcodebyexercisingitmanually.Thisisn'taverysustainableapproachasourapplicationbecomeslarger.Ideally,weshouldregularlyexerciseallthefunctionalityofourapplicationtocheckforregressions.Thiswouldquicklybecomeprohibitivelytime-consumingifwecontinuedtouseonlymanualtesting.Itismuchmoreeffectivetomaintainasuiteofautomatedtests.Thesealsobringmanyotherbenefits,forexample,actingasdocumentationofourcodeforotherdevelopers.
Inthischapter,wewillcoverthefollowingtopics:
WritingautomatedunittestsforourapplicationIntroducingnewlibrariestohelpuswritemoredescriptivetestsSeeinghowtocreateandusetestdoublesinJavaScriptExercisingourapplication'swebinterfaceusingHTTPclienttestsAddingfull-stackintegrationtestsusingbrowserautomationEstablishingastructureforwritingfurthertestsasweexpandourcodebase
WritingasimpletestinNode.jsNode.jscomeswithabuilt-inmodulecalledassertthatcanbeusedfortesting.WecanuseittowriteasimpletestforthegamesservicethatwewroteinChapter5,BuildingDynamicWebsites.WeaddthefollowingcodeundergameServiceTest.js:
'usestrict';
letassert=require('assert');
letservice=require('./services/games.js')
//Given
service.create('firstUserId','testing');
//When
letgames=service.availableTo('secondUserId');
//Then
assert.equal(games.length,1);
letgame=games[0];
assert.equal(game.setBy,'firstUserId');
assert.equal(game.word,'TESTING');
Notethattheassert.equalfunctiontakestheactualvalueasthefirstargumentandtheexpectedvalueasthesecondargument.ThisistheoppositewayaroundtoJUnit'sbuilt-inAssert.Equals,andtheclassic-styleAssert.AreEqualinNUnit.It'simportanttogettheseparameterstherightwayaroundsothattheyappearcorrectlyinerrormessageswhenanassertionfails.
Tip
Given,When,Then
TheGiven,When,andThencommentsintheprecedingtestarenotspecifictoJavaScriptoranyofthetestframeworkswe'llbeusing,butaregenerallyagoodtoolforstructuringteststokeepthemfocusedandreadable.
Wecannowverifyourcodeusingthefollowingcommand:
>nodegameServiceTest.js
>echo%errorlevel%
Anexitcodeof0indicatesthatourtestcompletedsuccessfullywithoutanyerrors.Althoughwehaven'tbeenfollowingtest-drivendevelopment(writingafailingtestfirstbeforeaddinganynewcode),it'sstillimportanttoseeeachtestfailtoconfirmthatit'stestingsomething.TryalteringtheavailableTofunctioninservices/games.jstoreturnanemptyarray,andrunthetestagain.
Notonlydowenowgetanon-zeroexitcode,butwealsogetanerrorcontainingourassertionfailure.Ourtestoutputstillisn'tparticularcompelling,though.Also,thelackofstructureinourtestscriptwillmakeithardertonavigateasweaddmoretests.Wecanaddressbothofthese
StructuringthecodebasefortestsAswewritemoretestsforourapplication,we'llbenefitfromhavingmorestructuretoourtests.It'scommontohaveatleastonetestfileperproductionmodule.Itwillalsobeusefultohaveawayofrunningallofourtestsandseeingtheoverallresult.
We'regoingtostartaddingtestsunderatestdirectory.Fromthispointoninthebook,we'realsogoingtokeepallofourapplicationcodeunderasrcdirectory.Thiswillmakeiteasiertonavigateourcodebaseandtokeepproductionandtestcodeseparate.
Ifyou'refollowingalongwiththebookatthispoint,youshouldmoveapp.jsandallthefolders(apartfromthebinfolder)underanewsrcdirectory,andupdatethestartupscriptasfollowsinbin/www:
varapp=require('../src/app');
vardebug=require('debug')('hangman:server');
varhttp=require('http');
WritingBDD-styletestswithMochaFromC#orJava,youmaybemostfamiliarwiththexUnit-styleoftestsusedbyNUnit,JUnit,andsoon.Thisstylestructurestestsintoclasses,andturnsmethodnamesintotestnames.Thiscanbeabitrestrictive,andisn'tcommoninJavaScripttesting.JavaScripttestframeworksmakeuseofthelessstructured,andmoredynamic,natureofthelanguagetoallowmoreflexibility.
ThereareseveraldifferentstylesforwritingtestsinJavaScript.Themostcommonistheso-calledbehavior-drivendevelopment(BDD)styleinwhichwedescribethebehaviorofourapplicationinplainEnglish.ThisisthedefaultstyleofthemostpopularJavaScripttestingframeworks.Itisalsocommoninframeworksforotherprogrammingplatforms,mostnotablyRSpecforRuby.
We'llbeusingapopulartestframeworknamedMocha.Let'sfirstaddthistoourapplication:
>npminstallmocha--save-dev
Notethat--save-devaddsMochatoourpackage.jsonfileasadevelopmentdependency.Thisindicatesthatit'snotneededinourproductioncode,andnpmdoesn'tneedtoinstallitinproductionenvironments.We'llalsoupdatethisfiletoletnpmrunourtestsusingMocha,byaddingatestscriptasfollows:
"scripts":{
"start":"node./bin/www",
"test":"nodenode_modules/mocha/bin/mochatest/**/*.js"
},
Thistellsnpmtoexecutescriptsunderthe/test/directoryastestsusingMochawhenwerunnpmtestfromthecommandline.
Note
MochaandJasmine
TherearealargenumberofdifferenttestingframeworksavailableforJavaScript.Themostwell-establishedareJasmineandMocha.Theyhavecomparablefeaturesandbothsupportthesamesyntaxforwritingtests.Theyarebothwell-documented,andswitchingbetweenthetwoiseasy.
Jasminewasoriginallyaimedmoreattestingclient-sideJavaScriptinthebrowser.Mochawasoriginallymorefocusedontestingserver-sideNode.jscode.
Nowadays,bothframeworksarewell-suitedforeitherenvironment.Jasminealsohasmorebatteriesincluded,whichcanmakeitquickertogetstartedwith.Mochadelegatesmorefeaturestootherlibraries,givingtheusermorechoiceabouthowtheyprefertowritetests.
Nowwejustneedtoaddsometests!Mochaprovidesglobalfunctionsnameddescribeanditforstructuringourtests.Thesefunctionseachtaketwoarguments:astringdescribingthebehavior
ofourapplicationandacallbackdefiningthetestsforthatbehavior.ThefollowingcodesnippetshowsourprevioustestrewrittenusingMocha.Weaddthefollowingcodeundertest/services/games.js:
'usestrict';
constassert=require('assert');
constservice=require('../../src/services/games.js');
describe('Gameservice',()=>{
constfirstUserId='user-id-1';
constsecondUserId='user-id-2';
describe('listofavailablegames',()=>{
it('shouldincludegamessetbyotherusers',()=>{
//Given
service.create(firstUserId,'testing');
//When
constgames=service.availableTo(secondUserId);
//Then
assert.equal(games.length,1);
constgame=games[0];
assert.equal(game.setBy,firstUserId);
assert.equal(game.word,'TESTING');
});
});
});
Nowtryrunningtheprevioustestusingnpmtest.Youshouldseeoutputlikethefollowing(theexactappearancewilldependonwhatconsoleyouareusing):
Notehowwegetamuchmoredescriptiveoutputofourtests.Alsonotetheuseofnesteddescribecallbacksinourtesttobuildupadescriptionofourapplication.Thebenefitofthisbecomesclearerasweaddmoretests.Tryaddingthefollowingtestafterthefirsttest:
it('shouldnotincludegamessetbythesameuser',()=>{
//Given
service.create(firstUserId,'first');
service.create(secondUserId,'second');
//When
constgames=service.availableTo(secondUserId);
//Then
assert.equal(games.length,1);
constgame=games[0];
assert.notEqual(game.setBy,secondUserId);
});
Runthetestsagainusingnpmtest.Thistime,wegetatestfailurefromMocha:
ResettingstatebetweentestsOursecondtestfailsbecauseitretrievestwogamesfromtheservice.Butthisisnotbecauseourproductioncodeisfailingtofiltergamescorrectly.Infact,therearetwogamescreatedbythefirstuser.Oneofthesehasbeencarriedoverfromtheprevioustest.
It'simportantforteststobeindependentandisolatedfromeachother.Tothisend,weneedtocleanupanystatebetweentests.Inthiscase,wewanttodeleteallthegameswecreated.Thegamesservicedoesn'tgiveusamethodforclearingallgames.Wecanonlyremoveindividualgamesafterretrievingthem.Thereareafewoptionsavailabletoushere:
Wecouldkeeptrackofallthegameswecreateduringeachtestanddeletethemallattheend.Thismightseemthemostobvioussolution,butit'sabitfragile.Itwouldbeeasytomissasinglegamethatmightcauseconfusingtestfailureslater.Wecouldrewritethegamesservicemoduletoexportafunctionforcreatinganewservice,andinstantiateanewserviceforeachtest.Ingeneral,it'sagoodideatotryandisolatetestsbycreatingfreshobjectsundereachtest.However,thisisonlyusefuliftheobjectdoesn'tstoreanyexternalstate.Wemaywellwanttochangetheimplementationofthegamesservicelater,tostoredataexternallyinapersistentdatastore.Wecouldaddaclearmethodtothegamesservicetowipeoutallitsdata.It'snotwrongtocreatemethodslikethisforthepurposesofsupportingtests.However,it'spreferabletointeractwiththeapplicationviaitsexistingAPIifpossible.
Thegamesservicedoesofferawayofretrievingallcurrentgames.WejustneedtopassinauserIDthatdoesn'tmatchthesetterofanygame.Wecanthengothroughanddeleteallgames.Wewanttodothisbeforeeverytest,whichwecandousingMocha'sbeforeEachhook:
describe('Gameservice',()=>{
constfirstUserId='user-id-1';
constsecondUserId='user-id-2';
beforeEach(()=>{
letgamesCreated=service.availableTo("not-a-user");
gamesCreated.forEach(game=>game.remove());
});
describe('listofavailablegames',()=>{
Ifwere-runourtests,theynowbothpasscorrectly.ThereisalsoanafterEachhookinMocha,whichwecouldhaveusedinstead.Thiswouldhaveworked,butit'ssaferforteststodefendthemselvesbycleaningupfirst,ratherthanrelyingonotherteststocleanupafterthemselves.
UsingChaiforassertionsAnotherwaytomakeourtestsmoredescriptiveishowwewriteourassertions.Althoughthebuilt-inNode.jsassertmodulehasbeenusefulsofar,itisabitlimited.Itonlycontainsasmallnumberofsimplemethodsforbasicassertions.
YoumayhaveexperienceofFluentAssertionsorNUnit'sConstraintmodelin.NET,orAssertJinJava.Comparedtothese,theNode.jsassertmodulemightseemquiteprimitive.
ThereareseveralassertionframeworksavailableforJavaScript.We'llbeusingChai(http://chaijs.com),whichsupportsthreedifferentstylesforwritingassertions.TheassertstylefollowsthetraditionalxUnitassertions,asinJUnit,ortheclassicmodelofNUnit.Theshouldandexpectstylesprovideanaturallanguageinterfaceforbuildingmoredescriptiveassertions.
Anyofthesestylesisaperfectlyvalidchoiceforwritingtestassertions.Theimportantthingistopickastyleforyourcodebaseanduseitconsistently.WewillbeusingChai'sexpectsyntaxthroughoutthisbook.ThisisoneofthemorecommonstylesinJavaScripttesting.TheJasminetestframeworkhasbuilt-inassertionsthatfollowasimilarstyle.
Let'sfirstinstallChaibyrunningthefollowingonthecommandline:
>npminstallchai--save-dev
Thenupdateourteststouseit:
constexpect=require('chai').expect;
constservice=require('../../src/services/games.js');
...
it('shouldincludegamescreatedbyotherusers',()=>{
//Given
service.create(firstUserId,'testing');
//When
constgames=service.availableTo(secondUserId);
//Then
expect(games.length).to.equal(1);
constgame=games[0];
expect(game.setBy).to.equal(firstUserId);
expect(game.word).to.equal('TESTING');
});
it('shouldnotincludegamescreatedbythesameuser',()=>{
//Given
service.create(firstUserId,'first');
service.create(secondUserId,'second');
//When
constgames=service.availableTo(secondUserId);
//Then
expect(games.length).to.equal(1);
letgame=games[0];
expect(game.setBy).not.to.equal(secondUserId);
});
Thechangeisn'tparticularlydramaticatthispointaswe'reonlymakingsimpleassertions.Butthenaturallanguageinterfacewillallowustospecifymoredetailedassertionsinadescriptiveway.
CreatingtestdoublesTherearemoretestswecouldwriteforthegamesservice,butlet'slookatadifferentmodulefornow.Howwouldwegoabouttestingourusersmiddleware?Thefollowingcodeisfrommiddleware/users.js:
module.exports=function(req,res,next){
letuserId=req.cookies.userId;
if(!userId){
userId=uuid.v4();
res.cookie('userId',userId);
}
req.user={
id:userId
};
next();
};
Inordertotestthisclass,wewillneedtopassinargumentsforthereq,res,andnextparameterswithwhichourcodeinteracts.Wedon'thavearealrequest,response,ormiddlewarepipelineavailable,soweneedtocreatesomestand-invaluesinstead.Stand-invaluessuchasthisaregenerallycalledtestdoubles.Ourcodereadsanattributefromtherequestandcallsthecookiemethodontheresponse.Wecancreatetestdoublesfortheseasfollows,inanewtestscriptundertest/middleware/users.js:
'usestrict';
constmiddleware=require('../../middleware/users.js');
constexpect=require('chai').expect;
describe('Usersmiddleware',()=>{
constdefaultUserId='user-id-1';
letrequest,response;
beforeEach(()=>{
request={cookies:{}};
response={cookie:()=>{}};
});
it('iftheuseralreadysignedin,readstheirIDfromacookieand
exposestheuserontherequest',()=>{
//Given
request.cookies.userId=defaultUserId;
//When
middleware(request,response,()=>{});
//Then
expect(request.user).to.exist;
expect(request.user.id).to.equal(defaultUserId);
});
});
Here,wesimplycreateaplainJavaScriptobjecttorepresenttherequest.Thisallowsustoverifythattheproductioncodereadsfrom,andwritesto,therequestpropertiescorrectly.Wejustpassintheminimumpossibleinputfortheresponseobjectandthenextfunctiontoallowthecodetoexecute.ThisisveryeasytodoinJavaScript,partlybecauseitisnotstaticallytyped.CreatingtestdoubleslikethisinC#orJavacanbealotmoreworkasthecompilerwillinsistonthetestdoublesmatchingthecorrespondingparametertypes.
Wealsoneedtotestthatourmiddlewarecallsthenextmiddlewareinthechain,asthisisimportantbehavior.Thisisslightlymorecomplexthanjustcreatinganobjectwithsimpleproperties.Wecanstillcreateasuitabletestdoublebydefininganewfunctionthatrecordswhenitiscalled(thiskindoftestdoubleiscalledaspy):
it('callsthenextmiddlewareinthechain',()=>{
//Given
letcalledNext=false;
constnext=()=>calledNext=true;
//When
middleware(request,response,next);
//Then
expect(calledNext).to.be.true;
});
Thisworksperfectlywell,butwillbecomemorecumbersomeifwewanttotestmorecomplexcalls,forexample,ifwewanttocheckformultiplecallsormakefurtherassertionsabouttheargumentspassedin.Wecansimplifythisbymakinguseofaframeworktocreatetestdoublesforus.
CreatingtestdoublesusingSinon.JSSinon.JSisaframeworkforcreatingallkindsoftestdoubles.Let'sfirstinstallitintoourapplicationbyrunningthefollowingonthecommandline:
>npminstallsinon--save-dev
Nowlet'ssimplifyourprevioustestandwriteamorecomplextestusingtestdoublescreatedbySinon.JS:
constexpect=require('chai').expect;
constsinon=require('sinon');
...
it('callsthenextmiddlewareinthechain',()=>{
//Given
constnext=sinon.spy();
//When
middleware(request,{},next);
//Then
expect(next.called).to.be.true;
});
it('iftheuserisnotalreadysignedin,'+
'createsanewuseridandstoresitinacookie',()=>{
//Given
request.cookies.userId=undefined;
response={cookie:sinon.spy()};
//When
middleware(request,response,()=>{});
//Then
expect(request.user).to.exist;
constnewUserId=request.user.id;
expect(newUserId).to.exist;
expect(response.cookie.calledWith(
'userId',newUserId)).to.be.true;
});
Sinon.JSspieskeeptrackofthedetailsofallcallsmadetothemandprovideaconvenientAPIforcheckingthese.Thisallowsustokeepourtestcodesimpleandreadable.TherearemanymorepropertiesthanjustthecalledandcalledWithuserhere.TakealookattheSinon.JSdocumentationathttp://sinonjs.org/docs/#spies-apitoseesomeoftheotherwayswecanverifythecallsmadeagainstaspy.
Note
Spies,stubs,andmocks
IfyoureadmoreoftheSinon.JSdocumentation,you'llseethatit'sveryexplicitaboutthedifferencebetweenspies,stubs,andmocks.ThisisincontrasttomostpopulartestdoubleframeworksinJavaand.NET,whichtendtocallalltestdoublesbythesamename(typicallymockorfake).Inrealitythough,mostinstancesoftestdoublestypicallyonlyactasaspy(usedforverifyingside-effects)orastub(usedforprovidingdata,orthrowingexceptionstotesterror-handling).Atruemockverifiesaspecificsequenceofcallsandreturnsspecificdatatothecodeundertest.AlthoughsomeoftheearlymockingframeworksinJavaand.NETonlysupportedthistypeoftestdouble(nowsometimescalledastrictmock),itisn'tcommonpracticeanymore.Thisisbecauseitquitetightlycouplestestandproductioncodeandmakesrefactoringmoredifficult.It'sespeciallyraretohavemorethanonemock(asopposedtojustastuborspy)inasingletest.
TestinganExpressapplicationWhileusingSinon.JSmakesourtestsneater,theystilldependonthedetailsoftheExpressmiddlewareAPIandhowwe'reusingit.Thismightbeappropriateforourmiddlewaremoduleaswewanttoensurethatitfulfillsaparticularcontract(especiallycallingnextandsettingrequest.user).Formostmiddleware,though,especiallyourroutes,thisapproachwouldcoupleourteststoocloselytoourimplementation.
ItwouldbebettertotesttheactualbehaviorofeachroutebymakingHTTPrequeststoitandexaminingtheresponses,ratherthancheckingforspecificlow-levelinteractionswiththerequestandresponseobjects.Thisgivesusmoreflexibilitytochangeourimplementationandrefactorourcode,withoutneedingtochangethetests.Thus,ourtestscansupportthisprocess(bycatchingregressions)ratherthanhinderingit(byhavingtobeupdatedtomatchourimplementation).
Onotherplatforms,testingawholeapplicationcanbequiteaheavyweightprocess.Itispossibletostartupaserverinprocess,forexample,usingJettyinJavaorKatanain.NET.Newerapplicationframeworks,suchasSpringBootorNancyFx,alsomakethisprocesseasier.Thesearestilllikelytoberelativelyslowandresource-intensivetests,though.
InNode.js,startingupanapplicationserveriseasyandverylightweight.Wejustusethesamehttp.createServercallaswe'veseenbefore,andpassitanapplication.Totestourrouteinisolation,we'llbootstrapanewapplicationcontainingjustthisroute.Let'sseehowwecanusethistotestthedeleteendpointofourgamesroute.Weaddthefollowingcodeundertest/routes/games.js:
'usestrict';
consthttp=require('http');
constexpress=require('express');
constbodyParser=require('body-parser');
constexpect=require('chai').expect;
constgamesService=require('../../src/services/games.js');
constTEST_PORT=5000,userId='test-user-id';
describe('/games',()=>{
letserver;
constmakeRequest=(method,path,callback)=>{
http.request({
method:method,
port:TEST_PORT,
path:path
},callback).end();
};
before(done=>{
constapp=express();
app.use(bodyParser.json());
app.use((req,res,next)=>{
req.user={id:userId};next();
});
constgames=require('../../src/routes/games.js');
app.use('/games',games);
server=http.createServer(app).listen(TEST_PORT,done);
});
afterEach(()=>{
constgamesCreated=gamesService.availableTo("non-user");
gamesCreated.forEach(game=>game.remove());
});
after(done=>{
server.close(done);
});
describe('/:idDELETE',()=>{
it('shouldallowuserstodeletetheirowngames',done=>{
constgame=gamesService.create(userId,'test');
makeRequest('DELETE','/games/'+game.id,response=>{
expect(response.statusCode).to.equal(200);
expect(gamesService.createdBy(userId)).to.be.empty;
done();
});
});
});
});
Thismightseemlikequitealotofcode,butrememberthatwe'refiringupanentireapplicationhere.Also,mostofthiscodewillbereusedformultipletests.Let'sworkthroughwhatitdoes.
Thebeforecallbackcreatesourserver,justaswesawinChapter2,GettingStartedwithNode.js,listeningonaspecialportforusebyourtests.Italsosetsupsomestubmiddlewaretosimulateacurrentuserontherequest.TheafterEachcallbackclearsupanycreatedgames(aswesawbeforeinthetestofthegamesservice).Notethatsincewe'rerunninginthesameprocess,wecantriviallyinteractwiththesamedatalayerthatourapplicationisusing.Finally,theafterfunctionaskstheservertostoplisteningforconnections.
Thetestitselfisverysimple:wejustcreateagamesetbythecurrentuser(asinourservicetestsbefore)andthenissuearequesttodeleteit.ThismakesuseofourownmakeRequestfunction,whichsimplycallsthroughtoNode'shttp.request.Wecantheninspecttheresponseobjecttocheckfortheappropriatestatuscode,andchecktheserviceforthedesiredeffect.
Tip
WritingasynchronoustestsinMocha
NoticethatourtestandallofthecallbackstoMocha'shookfunctionsdiscussedabove(exceptforafterEach)takeadoneparameter.Thisisbecauseallofthesetestsperformsome
asynchronouswork.Mochamakesitveryeasytowriteasynchronoustestsorhooks:youjustmakeyourcallbackfunctiontakeasingleparameter(calleddonebyconvention),andcallitwhenprocessingiscomplete.Ifit'snotcalledwithinatimeout(whichdefaultsto2secondsbutcanbechanged),thenMochafailsthetest.
Let'srunourtestsagainusingthenpmtestcommand.Noticethatallofthetestsstillfinishveryquickly(tensofmillisecondsonmymachine),eventhoughwe'restartingupourwholeserver-sideapplication.Youmayalsonoticetheoutputisabitmessyduetologoutputfromtheserver.Wecaneasilysuppressthisbyupdatingapp.jsasfollows:
//app.use(favicon(path.join(__dirname,'public','favicon.ico')));
if(app.get('env')==='development'){
app.use(logger('dev'));
}
app.use(bodyParser.json());
The'env'propertyofanExpressapplicationcomesfromtheNODE_ENVenvironmentvariable(ordefaultstodevelopmentifthisisnotpresent).Thisisusefulfordifferentiatingbetweenproductionanddevelopmentenvironments.Sinceitdefaultstodevelopment,wealsoneedtosetittosomethingelseinordertosuppressthislogginginourtests.Wecandothisbyupdatingourtestscriptinpackage.jsonasfollows:
"scripts":{
"start":"node./bin/www",
"test":"setNODE_ENV=test&&nodenode_modules/mocha/bin/mocha
test/**/*.js"
},
SimplifyingtestsusingSuperAgentWhileourtestsarefast,andsettinguptheserverisquitestraightforward,wedohavequitealotofcodeformakingrequeststotheserverandhandlingresponses.Thiswouldbecomemorecomplexifweneededtomakeawidervarietyofrequests,orwereinterestedinmorethanjusttheresponsestatuscodeorheaders.
WecansimplifyourtestsbyusingalibrarythatprovidesasimplerAPIforcommunicatingwiththeserver.SuperAgent(https://visionmedia.github.io/superagent/)isaJavaScriptlibrarythatprovidesafluent,readablesyntaxformakingHTTPrequests.ThiscanbeusedforAjaxrequestsinthebrowser,orforrequestsinaNode.jsapplicationaswe'redoinghere.
We'llmakeuseofSuperAgentthroughalightweightwrappercalledSuperTest(https://github.com/visionmedia/supertest),whichmakestestingNode.js-basedHTTPapplicationsevenmoreconvenient.
First,weaddSuperTestintoourapplicationusingnpm,byrunningthefollowingonthecommandline:
>npminstallsupertest--save-dev
Nowwecanrewriteourtestsasfollows:
'usestrict';
constexpress=require('express');
constbodyParser=require('body-parser');
constrequest=require('supertest');
constexpect=require('chai').expect;
constgamesService=require('../../src/services/games.js');
constuserId='test-user-id';
describe('/games',()=>{
letagent,app;
before(()=>{
app=express();
app.use(bodyParser.json());
app.use((req,res,next)=>{
req.user={id:userId};next();
});
constgames=require('../../src/routes/games.js');
app.use('/games',games);
});
beforeEach(()=>{
agent=request.agent(app);
});
describe('/:idDELETE',()=>{
it('shouldallowuserstodeletetheirowngames',done=>{
constgame=gamesService.create(userId,'test');
agent
.delete('/games/'+game.id)
.expect(200)
.expect(()=>
expect(gamesService.createdBy(userId)).to.be.empty)
.end(done);
});
});
});
SuperTestandSuperAgenttakecareofstartinguptheserverforourapplication,andprovideamuchsimplerAPIformakingrequests.Notetheuseofarequestagent,whichrepresentsasinglebrowsersession.
SuperAgentprovidesanumberoffunctions(get,post,delete,andsoon)formakingHTTPrequests.Thesecanbechainedwithcallstotheexpectfunction(nottobeconfusedwithChai'sexpect)toverifypropertiesoftheresponse,suchasthestatuscode.Wecanalsopassinacallbacktomakespecificchecksabouttheresponse,orverifyside-effects(aswedointhepreviousexample).
Notethatitisimportanttoalwayscalltheendfunctiontomakesureanyexpectationerrorsarethrownandfailthetest.WecanpassMocha'sdonecallbacktoendthetestwhentherequestiscompleted.
Nowthatwe'vesimplifiedourtestcode,wecaneasilyaddmoretestsforourroutes.Forexample,let'saddsometeststocoverthenegativecasesofourdeleteendpoint:
it('shouldnotallowuserstodeletegamesthattheydidnotset',done
=>{
constgame=gamesService.create('another-user-id','test');
agent
.delete('/games/'+game.id)
.expect(403)
.expect(()=>expect(gamesService.get(game.id).ok))
.end(done);
});
it('shouldreturna404forrequeststodeleteagamethatnolonger
exists',done=>{
constgame=gamesService.create(userId,'test');
agent
.delete(`/games/${game.id}`)
.expect(200)
.end(function(err){
if(err){
done(err);
}else{
agent
Full-stacktestingwithPhantomJSWehavenowwrittenunittestsforlogicatthecoreofourapplicationandintegrationtestsforourserver-sideroutes.Wedon'tyethaveanyautomatedteststhatcoverourviewsandclient-sidescriptsasourmanualtestingthroughoutthepreviouschaptersdid.
Wecanwriteunittestsforclient-sidescriptsusingMocha.However,allofourcurrentclient-sidescriptsinteractwiththeserver,soaren'tgoodcandidatesforunittesting.Ourmanualtestsarereallyfull-stacktestsofourwholeapplication,includingtheinteractionbetweentheserverandtheclient.
Inordertoachievethisinanautomatedtest,wewillneedtousesomeformofbrowserautomation.PhantomJSisaheadlessbrowserwithaJavaScriptAPIthatallowsustoautomateitdirectly.Wecanwriteasimpletestforourgamepageusingthis.
First,we'llinstallPhantomJSwithinourprojectbyrunningthefollowingonthecommandline:
>npminstallphantomjs-prebuilt--save-dev
Note
PhantomJSisnotaNode.jsmodule.Itisastandalone,headlesswebbrowser.Thenpmmoduleisjustaconvenientwayofinstallingitandmakingitadependencyoftheproject.PhantomJScannotbeinvokedfromNode.js,excepttoexecuteitasaseparatechildprocess.
Nowwecanimplementatestasfollows,underintegration-test/game.js:
(function(){
'usestrict';
varexpect=require('chai').expect;
varpage=require('webpage').create();
varrootUrl='http://localhost:3000';
withGame('Example',function(){
expect(getText('#word')).to.equal('_______');
page.evaluate(function(){
$(document).ajaxComplete(window.callPhantom);
});
page.sendEvent('keydown',page.event.key.E);
page.onCallback=verify(function(){
expect(getText('#word')).to.equal('E_____E');
expect(getText('#missedLetters')).to.be.empty;
page.sendEvent('keydown',page.event.key.T);
page.onCallback=verify(function(){
expect(getText('#word')).to.equal('E_____E');
expect(getText('#missedLetters')).to.equal('T');
console.log('Testcompletedsuccessfully!');
phantom.exit();
});
});
});
functionwithGame(word,callback){
...
}
functiongetText(selector){
returnpage.evaluate(function(s){
return$(s).text();
},selector);
}
functionverify(expectations){
returnfunction(){
try{
expectations();
}catch(e){
console.log('Testfailed!');
handleError(e.message);
}
}
}
functionhandleError(message){
console.log(message);
phantom.exit(1);
}
phantom.onError=page.onError=handleError;
}());
Makesuretheapplicationisrunning(usingnpmstart),thenexecutethetestbyrunningthefollowingonthecommandline:
>nodenode_modules/phantomjs-prebuilt/bin/phantomjsintegration-test/game.js
Let'stakealookthroughthecodetounderstandhowitworks.Notethatwe'rerunninginthebrowserenvironmenthereratherthanNode.js,sofallbacktotheECMAScript5syntax(forexample,varinsteadoflet,andnoarrowfunctions).
TheomittedwithGamemethod(whichyoucanfindinthebook'scompanioncode)usesPhantomJStoloadtheindexviewandsubmitanewgame,thenclearsPhantomJS'scookiesandopensthegameasanewuser,beforeinvokingthecallbackpassedtowithGame.
Inourtest,wecreateagametoguessthewordexample,theninvokeJavaScriptwithinthepagetomakeassertionsaboutitscontents.ThegetTextfunctionusesPhantomJS'spage.evaluatefunctiontorunsomeJavaScriptwithinthecontextofthepage,andreturnavalue.Notethatthecallbackfunctionpassedtopage.evaluatedoesnothaveaccesstothewiderexecutioncontext
ofourscript.Wecan,however,specifyadditionalargumentstothepage.evaluatecall,whichishowwepassintheselectorforjQuery.
Wethenusepage.evaluateagaintosetupacallbackeachtimeanAjaxrequestcompletes.Here,weusewindow.callPhantom,whichexecuteswithinthecontextofthepage,andtriggerspage.onCallback,whichexecuteswithinthecontextofourtest.
Finally,weusepage.sendEventtotriggerakeyboardeventinthebrowser.NotethatthisisnotthesameasusingpureJavaScriptwithinthebrowsertotriggeraDOMevent,butisaninstructiondirectlytoPhantomJStosimulatethekeypresseventasifithadcomefromtheuser.
Ifweputallthistogether,wegetthefollowing:
Weusepage.sendEventtosimulatepressingakeyboardkeyThiscausesourproductioncodetosendoffanAjaxrequestWhenthisrequestcompletes,window.callPhantomisinvokedinthecontextofthebrowserThiscausesPhantomJStoinvokeourpage.onCallbackfunctionWethenusejQuerywithinpage.evaluate(viagetText)toretrievevaluesfromthepage
Theremainingcontentsofthefile(verifyandhandleError)ensurethatPhantomJSwritesallerrorstotheconsoleandsetsanappropriateexitcodeinthecaseofafailure.
SummaryInthischapter,wehavelearnedhowtowriteunittestsinNode.js,usedMochaandChaitowritemoredescriptivetests,createdtestdoublesusingSinon.JS,writtenapplicationleveltestsusingSuperAgentandSuperTest,andimplementedafull-stacktestinPhantomJS.
Althoughwehavetestsateachlayerofourapplicationnow,wehaven'tyetcoveredallofourcode.Itwouldbeusefultofindanygapswhereweshouldwritemoretests.Wealsohavetoinvokeafewdifferentcommandstorunallofourunitandintegrationtests.Inthenextchapter,we'llseehowtoautomatetheseandotherprocessesaspartofacontinuousintegrationbuild.
Chapter7.SettingupanAutomatedBuildInthepreviouschapter,wetookamajorstepfromademoapplicationtoamaintainablecodebasebystartingtowriteautomatedtests.Anotherimportantcomponentofreal-worldsoftwareprojectsisbuildautomation.
Automatedbuildsallowawholeteamtoworkonaprojectinaconsistentmanner.Astandardizedwayofexecutingcommontasksmakesiteasierfornewdeveloperstogetstarted.Italsoavoidsannoyingissueswithdevelopersgettingdifferentresultsforspuriousreasons.
Inthischapter,wewillcoverthefollowingtopics:
ConfiguringanintegrationservertobuildandrunourtestsautomaticallySettingupanautomatedtaskrunnertosimplifytheexecutionofourtestsAutomatingmoretaskstohelpmaintaincodingstandardsandtestcoverage
SettingupanintegrationserverBuildandtestautomationallowcodechangestobeverifiedbyanintegrationserver,anautomatedserverindependentofindividualdevelopers'machines.Thishelpskeeptheprojectstablebycatchingerrorsorregressionsearlyon.Theintegrationservercanautomaticallyalertthedeveloperwhointroducedtheproblem.Theythenhaveachancetofixtheproblembeforeitcausesissuesfortherestoftheteamortheprojectasawhole.
BuildingthecodebaseandrunningtestsautomaticallyoneachcommitiscalledContinuousIntegration(CI).TherearemanyCI/buildserversavailable.Thesecanbeself-hostedorprovidedasathird-partyservice.ExamplesthatyoumayhaveusedbeforeincludeJenkins(formerlyHudson),AtlassianBamboo,JetBrainsTeamCity,andMicrosoft'sTeamFoundationServer.
We'regoingtobeusingTravisCI(https://travis-ci.org/),whichisahostedserviceforrunningautomatedbuilds.Itisfreeforusewithpublicsourcecoderepositories.InordertouseTravisCI'sfreeservice,weneedtohostourapplication'scodeinapublicGitHubrepository.
SettingupapublicGitHubrepositoryIfyouhaveyourownversionoftheexampleapplicationcodefromfollowingalongwiththebooksofar,andarealreadyfamiliarwithGitHub,youcanpushyourcodetoanewGitHubrepositoryofyourown.Otherwise,youcanforkoneoftheexamplechapterrepositories.
Usehttps://github.com/NodeJsForDevelopers/chapter06/ifyouwanttofollowalongwiththechangesinthischapter.ThiscontainstheexamplecodefromtheendofChapter6,TestingNode.jsApplications,whichwewillbuildoninthischapter.YoucancreateyourownforkofthisrepositoryusingtheForkbuttononGitHub.Thisshouldbevisibleatthetop-rightofthescreenwhenvisitingtheURLmentionedearlier:
ThiswillcreateanewrepositoryunderyourownGitHubaccount,usingtheexamplecodeasastartingpoint.
Note
Thisisallyouneedtogetstartedinthischapter.However,ifyouarenotalreadyfamiliarwithGitand/orGitHubandwouldliketoknowmore,youcanfindmoreinformationathttps://help.github.com/.
BuildingaprojectonTravisCIWe'llnowsetupabuildforourapplicationonTravisCI.Ifyoucreatedyourownpublicrepositoryintheprevioussection,youcantrythisoutforyourself.Visithttps://travis-ci.organdsigninwithGitHub.Youshouldseeaprofilepagelistingyourrepositories.Enabletherepositoryyoujustcreated.
WehavetocreateasimpleconfigfiletotellTravisCIinwhatenvironment(s)tobuildourapplication.Createafileintherootoftheprojectasfollows(notetheleadingdotinthefilename.travis.yml):
language:node_js
node_js:
-6
-4
ThistellsTravisCItobuildourprojectwiththecurrentstableandlong-termsupportversionsofNode.js(atthetimeofwriting).Ifyou'refamiliarwithGit,youcanmakethischangeinalocalcloneofyourrepository,commit,andpushittomaster.Ifyou'renewtoGit,theeasiestwaytocreatethisfileistonavigatetoyourrepositoryonhttps://github.comandclickontheNewfilebutton.Thiswillopenaweb-basededitorfromwhichyoucancreateandcommitthefile.
Onceyouhaveaddedthisfiletoyourrepository,visithttps://travis-ci.orgagain.Youshouldnowseeapassingbuildforyourrepository:
TravisCIbuiltourprojecttwice,onceforeachversionofNode.jsthatwespecified.Ifyouclickoneitherbuildyoucanseethecommand-lineoutput.NoticethatTravisCIautomaticallyranourtestsusingthestandardnpmtestcommand.
AutomatingthebuildprocesswithGulpIt'sgreatthatTravisCIrunsourtestsautomatically.Butthat'snottheonlytaskwewanttoautomate.Ofcourse,asJavaScriptisaninterpretedlanguage,wedon'thaveacompilestepinourbuildprocess.Thereareothertaskswewanttocarryoutthough,forexample,checkingourcodestyle,runningintegrationtests,andgatheringcodecoverage.Wecanmakeuseofabuildtooltoautomatethesetasksandallowustoruntheminaconsistentmanner.YoumayhaveusedMSBuildforthisin.NETbeforeorJavatoolssuchasMavenorGradle.
ThereareseveraldifferentbuildtoolsavailableforNode.js.ThetwomostpopularbyfarareGruntandGulp.Bothhavelargecommunitiesandanextensiverangeofpluginsforperformingdifferentoperations.Grunt'smodelhaseachoperationreadinginfilesandwritingbacktothefilesystem.GulpusesNode.jsstreamstopipeprocessingfromoneoperationtothenext.
Grunt'smodelisslightlysimplerandmaybeeasiertogetstartedwith,especiallyifyouhavemodestbuildrequirements.Gulp'smodelispotentiallyfasterforsometypesoftaskandcanreducetheamountofbuildconfigurationcodeyouneedtowrite.Bothareexcellent,well-supportedbuildtools.We'llbeusingGulp,buteverythingwedointhischaptercouldbeachievedwithGruntaswell.
RunningtestsusingGulpWefirstneedtoinstallGulp,bothglobally(toaddittoourpath)andintotheproject.ThenweaddGulppluginsforcontrollingMochaandenvironmentvariables:
>npminstall-ggulp-cli
>npminstallgulp@~3.x--save-dev
>npminstallgulp-mocha--save-dev
>npminstallgulp-env--save-dev
WenowaddaconfigurationfileforGulptoourproject.Gulpwilllookforafilewiththisnamebyconventionasgulpfile.js:
'usestrict';
constgulp=require('gulp');
constmocha=require('gulp-mocha');
constenv=require('gulp-env');
gulp.task('test',function(){
env({vars:{NODE_ENV:'test'}});
returngulp.src('test/**/*.js')
.pipe(mocha());
});
gulp.task('default',['test']);
Thiscreatesatesttaskandmakesanemptydefaulttasktorunit.The'default'tasknameisspecialandwillbeinvokedwhenwerungulpfromthecommandline.Wecannowremoveourtestscriptfrompackage.jsonandupdateour.travis.ymlfiletorunGulp:
language:node_js
before_script:
-npminstall-ggulp
script:gulp
node_js:
-6
-4
Thishasn'tgainedusmuchyet.Wenowjusthaveaslightlyshortercommandtoexecuteourtests.However,theuseofabuildtoolwillbecomemorevaluableasweaddmoretaskstoautomate.Let'slookatsomeoftheotherprocesseswemaywanttomakepartofourbuild.
CheckingcodestylewithESLintAlthoughwedon'tneedacompiler,wecanstillbenefitfromhavingthecomputerperformstaticanalysisofourcode.Lintingtoolsarecommoninmanylanguagesforspottingcommonprogrammingerrorsthatmayleadtosubtlebugsorconfusingcode.YoumaybefamiliarwithCodeRush,StyleCop,andothersfor.NET,orCheckStyle,Findbugs,Sonar,andothersforJava.
We'llbeusingaJavaScript/ECMAScriptlintingtoolcalledESLint.Let'sfirstinstallitglobally:
>npminstall-geslint
NowcreateaconfigfiletotellESLintwhatrulestouseas.eslintrc.json:
{
"extends":"eslint:recommended",
"env":{
"node":true,
"es6":true,
"mocha":true,
"browser":true,
"jquery":true
},
"rules":{
"semi":[2,"always"],
"quotes":[2,"single"]
}
}
Here,wetellESLinttouseitsstandardrecommendedrulesfortheenvironmentsthatweareusinginourscripts.Wealsotellittocheckforsemicolonsattheendsofstatementsandtoprefersinglequotes.YoucanrunESLintasfollows:
>eslint**/*.js
ESLintoutputsanyerrorsitfinds,includingthefollowing:
Anunusedfaviconlocalvariableinapp.jsTheunusednextparameterinvariousmiddlewarefunctionsTheuseofconsole.loginourPhantomJSintegrationtestTheuseofthephantomvariableinourPhantomJSintegrationtest
Thefirstoftheseistrivialtosolve:wecanjustremovethevariabledeclaration(thiswascreatedforusbytheexpressapplicationtemplateinChapter2,GettingStartedwithNode.js).Wecoulddothesameforthenextparametersonourmiddlewarefunctions.However,Iprefermiddlewarefunctionstohaveastandardandeasilyidentifiablesignature.Insteadofremovingthisparameter,wecantellESLinttoignorethisparticularparameterasfollows:
"rules":{
"semi":[2,"always"],
"quotes":[2,"single"],
"no-unused-vars":[2,{"argsIgnorePattern":"next"}]
}
ThelasttwobulletpointsbothrelatetoourPhantomJSintegrationtest.Thisisquiteaspecialfile,soherewe'llchangeESLint'sbehaviorforthisfilespecifically,usingacommentdirective.Wecanaddthefollowingdirectivesattheverytopoftheoffendingfile,integration-test/game.js:
/*eslint-envphantomjs*/
/*eslint-disableno-console*/
ThefirstofthesedirectivestellsESLintthatthisscriptfilewillruninthePhantomJSenvironment,wherethephantomvariablewillbeprovidedforus,soESLintdoesnotneedtowarnusagainstreferencingit.Theseconddirectivedisable'sESLint'sruleagainstusingconsolelogging.
IfyourunESLintagain,youshouldfindthattheerrorslistedpreviouslyhavedisappeared.Anyremainingerrorsshouldbesmallerissuessuchasmissingsemicolonsorinconsistentuseofquotes.Theseshouldbequicktofixmanually,butinfact,ESLintcandothisforus,aswe'llseeinthenextsection.
AutomaticallyfixingissuesinESLintESLintisabletoautomaticallycorrectsomeoftheissuesitfinds.IfESLintisnotcurrentlyreportinganyerrors,tryremovingasemicolonfromoneoftheproject'ssourcefiles.RunESLintandyoushouldseeanerrorforthis.
NowrunESLintwiththe--fixoptionasfollows:
>eslint**/*.js--fix
ESLintreplacesthesemicolonforus.NotallofESLint'srulescanbefixedinthisway,butmanyofthemcan.Itdependsonwhetherarule'serrorsalwayshaveasingleunambiguousfix.Thefulllistofrules,includingwhichonesarefixable,canbefoundontheESLintsiteathttp://eslint.org/docs/rules/.
YoushouldnowbeabletorunESLintwithnoerrorsorwarnings.ESLintisnowreadytopickuperrorsinanynewcodethatwewrite.
RunningESLintfromGulpIt'sslightlymessytospecifyspecialexclusionsforourPhantomintegrationtest.It'salsounfortunatethatwe'reenablingtheNode.js,Mocha,browser,andjQueryenvironmentsglobally.TheMochaenvironmentisonlyneededforourtestcode.ThebrowserandjQueryenvironmentsareonlyneedforourclient-sidecode,wheretheNode.jsenvironmentisnotneeded.
ThiswouldbeeasiertomanageifweranESLintseparatelyondifferentsetsoffiles.Thiswouldstarttobecometediousanderror-proneifwediditmanually.Butit'sagreatusecaseforabuildtool.WecansetupseparateESLintprofilesfordifferentsetsoffilesusingGulp.First,installtheGulpESLintplugin:
>npminstallgulp-eslint--save-dev
NowwecancreateGulptaskstolinteachsetofsources.Bydefault,thegulp-eslintpluginusesrulesfromour.eslintrc.jsonfile.So,wecancutthisdowntojusttherulesthatarerelevanttoallsources:
{
"extends":"eslint:recommended",
"rules":{
"no-unused-vars":[2,{"args":"after-used"}],
"quotes":[2,"single"],
"semi":[2,"always"]
}
}
WecanthenspecifytherelevantrulesorenvironmentsforeachsetofsourcesintheirownGulptask.Thisalsoallowsustoremovethespecialdirectivecommentsfromthetopofourintegrationtestscript:
consteslint=require('gulp-eslint');
gulp.task('lint-server',function(){
returngulp.src(['src/**/*.js','!src/public/**/*.js'])
.pipe(eslint({
envs:['es6','node'],
rules:{
'no-unused-vars':[2,{'argsIgnorePattern':'next'}]
}
}))
.pipe(eslint.format())
.pipe(eslint.failAfterError());
});
gulp.task('lint-client',function(){
returngulp.src('src/public/**/*.js')
.pipe(eslint({envs:['browser','jquery']}))
.pipe(eslint.format())
.pipe(eslint.failAfterError());
});
gulp.task('lint-test',function(){
returngulp.src('test/**/*.js')
.pipe(eslint({envs:['es6','node','mocha']}))
.pipe(eslint.format())
.pipe(eslint.failAfterError());
});
gulp.task('lint-integration-test',function(){
returngulp.src('integration-test/**/*.js')
.pipe(eslint({
envs:['browser','phantomjs','jquery'],
rules:{'no-console':0}
}))
.pipe(eslint.format())
.pipe(eslint.failAfterError());
});
Finally,wewireupthedependenciesbetweenourtasks:
gulp.task('test',['lint-test'],function(){
env({vars:{NODE_ENV:'test'}});
returngulp.src('test/**/*.js')
.pipe(mocha());
});
gulp.task('lint',[
'lint-server','lint-client','lint-test','lint-integration-test'
]);
gulp.task('default',['lint','test']);
Here,wemakethetesttaskdependonlint-testandcreateanewoveralllinttasktorunalloftheothersaspartofthedefaultbuild.TryrunningGulpandobservetheoutput.Notethatitkicksoffallthelinttasksinparallel,butwaitsforlint-testtocompletebeforerunningtests.Bydefault,Gulpwillruntasksconcurrentlyifpossible.Ifataskreturnsastream(theobjectobtainedfromgulp.src)attheend,Gulpisabletousethistodetectwhenthetaskfinishes.Gulpwillwaitforatasktofinishbeforestartinganytasksthatdependonit.
ToseehowESLintfailuresaffectGulp,let'saddanotherESLintruletoensuretheuseofJavaScript'sstrictmode,asdescribedinChapter3,AJavaScriptPrimer.Thefollowingcodeisfrom.eslintrc.json:
{
"extends":"eslint:recommended",
"rules":{
"no-unused-vars":[2,{"args":"after-used"}],
"quotes":[2,"single"],
"semi":[2,"always"],
"strict":[2,"safe"]
}
}
ESLintiscleverenoughtomakeuseofthespecifiedenvironmentforeachsetoffilestoworkouthowstrictmodeshouldbeapplied:atthetopoffunctionsforclient-sidescriptsandgloballyfor
filesthatwillbecomeNode.jsmodules.Italsospotswhenweunnecessarilyspecifystrictmodemultipletimes,globallyorinnestedfunctions.
WhenyouexecuteGulp,noticethatfailuresintheESLinttaskspreventthedependenttesttasksfromrunning.Ifyoufixthestrictmodeerrors,thenGulpwillrunsuccessfullyagain.
GatheringcodecoveragestatisticsAlthoughwehavesometestsforourapplication,theyarecertainlynotyetcomprehensive.Itwouldbeusefultobeabletoseewhatpartsofourcodearecoveredbytests.Forthis,we'lluseIstanbul,aJavaScriptcodecoveragetool.First,installthegulp-instanbulplugin:
>npminstallgulp-istanbul--save-dev
NowweneedtoaddaGulptasktoinstrumentourproductioncodeforcoverage:
constistanbul=require('gulp-istanbul');
...
gulp.task('instrument',function(){
returngulp.src('src/**/*.js')
.pipe(istanbul())
.pipe(istanbul.hookRequire())
});
Finally,weneedtoupdateourtesttasktooutputacoveragereportandfailthebuildifwearebelowourthreshold:
gulp.task('test',['lint-test','instrument'],function(){
gulp.src('test/**/*.js')
.pipe(mocha())
.pipe(istanbul.writeReports())
.pipe(istanbul.enforceThresholds({
thresholds:{global:90}
}));
});
Now,whenwerunGulp,threenewresultsoccur:
AcoveragesummaryappearsonthecommandlineAsetofcoveragereportsappearunderthecoveragefolderThebuildfailsbecausewearebelowthecoveragethreshold
Thebuildsummaryonthecommandlineisveryuseful.ThereisevenmoredetailintheHTMLreportthatappearsatcoverage/lcov-report/index.html(intheprojectdirectory).
Althoughweneedtoimproveourtestcoverage,wedon'twanttoleaveourbuildfailing.Fornow,we'llsetthecoveragetargetjustbelowourcurrentlevelsoitdoesn'tdropfurther.Wecandothiswiththeoptionspassedtoistanbul.enforceThresholds:
gulp.task('test',['lint-test','instrument'],function(){
returngulp.src('test/**/*.js')
.pipe(mocha())
.pipe(istanbul.writeReports())
.pipe(istanbul.enforceThresholds({
thresholds:{
RunningintegrationtestsfromGulpGulptasksarejustordinaryJavaScriptfunctions,socancontainanyfunctionalitywelike.Let'slookatamorecomplexusecase.We'llcreateataskthatstartsupourserver,runsintegrationtests,andthenclosestheserver.Forthis,we'llneedtheGulpShellplugin:
>npminstallgulp-shell--save-dev
First,weupdateourintegrationtestscriptsothatwecanpassintheportnumberofthetestserver.ThismakesuseofthePhantomJS'system'moduleasfollows(inintegration-test/game.js):
varrootUrl='http://localhost:'+
require('system').env.TEST_PORT||3000;
NowwecandefineaGulptasktoruntheserverandtheintegrationtest:
constshell=require('gulp-shell');
...
gulp.task('integration-test',
['lint-integration-test','test'],(done)=>{
constTEST_PORT=5000;
letserver=require('http')
.createServer(require('./src/app.js'))
.listen(TEST_PORT,function(){
gulp.src('integration-test/**/*.js')
.pipe(shell('nodenode_modules/phantomjs-prebuilt/bin/phantomjs
<%=file.path%>',{
env:{'TEST_PORT':TEST_PORT}
}))
.on('error',()=>server.close(done))
.on('end',()=>server.close(done))
});
});
Thislaunchestheapplicationandthenmakesuseofthegulp-shellplugintoexecuteourintegrationtestscripts.Finally,wemakesureweclosetheserverwhendone,passinginGulp'sasynccallback.Likereturningastream,usingthiscallbackallowsGulptoknowwhenthetaskhascompleted.
Wemakethistaskdependonthetesttasksothattheydon'tinterferewithoneanother.Wedon'tmakethispartofourdefaulttaskasit'samoreheavyweightoperation.Wedowantittorunonourbuildserverthough,sowe'lladditto.travis.ymlalongwiththedefaulttask:
language:node_js
before_script:
-npminstall-ggulp
script:gulpdefaultintegration-test
node_js:
-5
-4
Now,ifwepushtotheremotemaster,TravisCIwillrunstaticanalysisonourcode,executeallofourunitandintegrationtests,andchecktheunittestcoverage.
SummaryInthischapter,wehavesetupanintegrationbuildusingTravisCI,addedstaticanalysisofourcodeusingESLint,automatedourtestsandothertasksusingGulp,andstartedmeasuringourtestcoverageusingtheIstanbultool.
Nowthatwehavetheinfrastructureinplaceforstabledevelopment,wecanstartexpandingourproject.Inthenextchapter,we'llintroducepersistentdatastorestotheapplication.
Chapter8.MasteringAsynchronicityOurJavaScriptprimer(Chapter3,AJavaScriptPrimer)coveredalltheimportantconceptstoletusstartbuildingourapplication.ButthereisonefundamentalaspectofJavaScriptprogrammingworthexploringinmoredetail:asynchronicity.
Chapter1,WhyNode.js?,discussedtheasynchronousprogrammingmodelofNode.js.ItdescribedtheconsistentapproachusedthroughoutNode.jsAPIsandthird-partylibraries.Recallthateachasynchronousmethodtakesacallbackfunctionthatgetspassederrorandresultarguments,forexample,thefs.statfunctionwesawinChapter1,WhyNode.js?:
fs.stat('/hello/world',function(error,stats){
console.log('Filelastupdatedat:'+stats.mtime);
});
However,thecallbackpatternhassomeweaknesses.Performingerrorhandlingandcombiningresultsfrommultipleasynchronousoperationscanbecomequiteclumsy.TherearealternativeasynchronouspatternsavailableinJavaScriptthataddresstheseissues.Theideaofmultiplecompetingpatternsmightseemworryinginitself,though.HavingasingleconsistentapproachwasoneofthebenefitsofNode.jsdiscussedinChapter1,WhyNode.js?.
WeshouldalsorevisittheideaofNode.jsAPIsandlibrariesbeingasynchronousthroughout.Weneedtoconsiderhowthisappliestoourowncode.Thisisnotjustsomethingweneedtoworryaboutifwritingamoduleforusebyathird-party.Evenwithinourownapplications,mostmoduleswillneedtoexposetheirfunctionalitythroughanasynchronousinterface.Ifnot,weseverelylimitthefreedomofhowweimplementthesemodules.
Inthischapter,wewillcoverthefollowingtopics:
IntroducingasynchronousinterfacestoourownmodulesObservingsomeoftheweaknessesofthecallbackpatternRefactoringawayfromcallbackstomakeourasynchronouscodemorereadableSeeinghowwecanstillbenefitfromtheconsistencyofNode.js'sasynchronousprogrammingmodel
UsingthecallbackpatternforasynchronouscodeLet'slookatoneofthemethodsfromourgamesservice:
module.exports.get=(id)=>games.find(game=>game.id===id);
Theinterfaceofthisfunctionissynchronous:youcallitandgetavalueback.Chapter4,IntroducingNode.jsModules,introducedthegamesserviceasthemoduleresponsibleforhowwestoreourgames.Theinterfaceshouldn'tneedtochangeifwechangethestorageimplementation.Thisisn'tquitethecaseatthemoment,though.
Asdiscussedbefore,mostNode.jslibrariesareasynchronous.Synchronousinterfacescan'tmakeuseofasynchronousimplementations.Let'ssaythegetfunctionwantstomakeuseofanasynchronousmethodinathird-partydatastorelibrary.Whatwouldthatlooklike?Thecommentsinthefollowing(non-working)codedescribetheproblem:
module.exports.get=(id)=>{
datastore.getById(id,(err,result)=>{
//Resultavailable,butoutermethodhasalreadyreturned
});
return???;//Needtoreturnhere,buthavenoresultyet
};
Thisisaproblemingeneral,notjustinJavaScript.Inotherplatforms,youcoulddelayreturninguntiltheasynchronousoperationhascompleted.Thisturnsanasynchronousoperationintoablockingoperation.InNode.js(andotherJavaScriptenvironments),blockinginthiswayisnotanoption.Itwouldbeincompatiblewiththesingle-threaded,non-blocking,event-drivenexecutionmodel.
ExposingthecallbackpatternToallowourgamesservicetobeabletomakeuseofasynchronouslibraries,weneedtogiveitanasynchronousinterface.NotethatalmostalllibrariesintheNode.jsecosystemareasynchronous.Iftheyweren't,theywouldbelimitedinthesamewayasourgamesservicecurrentlyis.
Wecanrewritetheinterfaceofourgetfunctiontofollowthestandardasynchronouscallbackpattern.Let'sseewhateffectthishasonusinganasynchronousthird-partydatastorelibrary(again,thisisnon-workingcode,withafictionaldatastoreobject):
module.exports.get=(id,callback)=>{
datastore.getById(id,(err,result)=>{
//Cannowmakeuseoftheresultbypassingtothecallback
callback(err,result);
}
//Nolongerneedtoreturnhere
}
Ofcourse,inthiscasewecouldsimplifytheprecedingcodeasfollows:
module.exports.get=(id,callback)=>{
datastore.getById(id,callback);
}
Ingeneral,though,wemightwanttodosomemoreprocessingoftheresultfromathird-partylibrary.Soourfunctionmightlookmorelikethis:
module.exports.get=(id,callback)=>{
datastore.getById(id,(err,result)=>{
if(err){
callback(err);
}else{
callback(null,processResult(result));
}
}
}
AssumingprocessResultisinternaltoourmodule,it'sfineforittohaveasynchronousinterfacefornow.Ifitneedstodoasynchronousworklater,wecanchangeitsinterfacewithoutaffectingtheconsumersofourmodule.
Ourgamesservicemodule'spublicinterfacedoesneedtobeentirelyasynchronous,though.We'renotactuallychangingtheimplementationofthemoduleyet.Thismakesupdatingtheinterfacequitestraightforward.Wecanmakethefollowingchangesinsrc/services/games.js:
'usestrict';
constgames=[];
letnextId=1;
classGame{
...
remove(callback){
games.splice(games.indexOf(this),1);
callback();
}
}
module.exports.create=(userId,word,callback)=>{
constnewGame=newGame(nextId++,userId,word);
games.push(newGame);
callback(newGame);
};
module.exports.get=(id,callback)=>
callback(null,
games.find(game=>game.id===parseInt(id,10)));
module.exports.createdBy=(userId,callback)=>
callback(null,games.filter(game=>game.setBy===userId));
module.exports.availableTo=(userId,callback)=>
callback(null,games.filter(game=>game.setBy!==userId));
Notethatthisisslightlyunrealistic,though.Controlwouldnormallyreturntothecallerbeforeanasynchronousmethodcompletes.Wecanachievethisbyusingprocess.nextTicktoscheduletheexecutionofthecallbackonthenexttickoftheeventloop(refertoChapter1,WhyNode.js?,ifyouwantarefresherontheeventloop):
'usestrict';
constgames=[];
letnextId=1;
constasAsync=(callback,result)=>
process.nextTick(()=>callback(null,result));
classGame{
...
remove(callback){
games.splice(games.indexOf(this),1);
asAsync(callback);
}
}
module.exports.create=(userId,word,callback)=>{
letgame=newGame(nextId++,userId,word);
games.push(game);
asAsync(callback);
};
module.exports.get=(id,callback)=>
asAsync(callback,
games.find(game=>game.id===parseInt(id,10)));
module.exports.createdBy=(userId,callback)=>
asAsync(callback,games.filter(game=>game.setBy===userId));
module.exports.availableTo=(userId,callback)=>
asAsync(callback,games.filter(game=>game.setBy!==userId));
Updatingtherestofourapplicationtoconsumethisasynchronousinterfaceisatrickiertask.Thisiswhyitisworthalwayswritingmoduleinterfacestobeasynchronousfromthestart.Weshoulddefinitelyaddressthisbeforeexpandingourapplicationanyfurther.
ConsumingasynchronousinterfacesThegamesserviceiscalledbythegamesroute,theindexroute,andbyourtests.Let'slookatthecorrespondingchangestoeachoftheseinturn.Thefollowingcodeisfromsrc/routes/games.js:
'usestrict';
constexpress=require('express');
constrouter=express.Router();
constservice=require('../service/games.js');
router.post('/',function(req,res,next){
letword=req.body.word;
if(word&&/^[A-Za-z]{3,}$/.test(word)){
service.create(req.user.id,word,(err,game)=>{
if(err){
next(err);
}else{
res.redirect(`/games/${game.id}/created`);
}
});
}else{
res.status(400).send('Wordmustbeatleastthreecharacterslongand
containonlyletters');
}
});
constcheckGameExists=function(id,res,onSuccess,onError){
service.get(id,function(err,game){
if(err){
onError(err);
}else{
if(game){
onSuccess(game);
}else{
res.status(404).send('Non-existentgameID');
}
}
});
};
router.get('/:id',function(req,res,next){
checkGameExists(
req.params.id,
res,
game=>{...},
next);
});
router.post('/:id/guesses',function(req,res,next){
checkGameExists(
req.params.id,
res,
game=>{...},
next);
});
router.delete('/:id',function(req,res,next){
checkGameExists(
req.params.id,
res,
game=>{
if(game.setBy===req.user.id){
game.remove((err)=>{
if(err){
next(err);
}else{
res.send();
}
});
}else{
res.status(403).send('Youdon'thavepermission...');
}
},
next);
});
router.get('/:id/created',function(req,res,next){
checkGameExists(
req.params.id,
res,
game=>res.render('createdGame',game),
next);
});
module.exports=router;
Inthiscase,thechangesarestraightforward.Eachcalltoagamesservicefunctionnowpassesinacallback.Thecallbackcontainsthelogicthatusedtofollowthecalltothegamesservicefunction.Eachcallbackalsoneedstohandlethepossibilityofanerrorvalue.Inthiscase,wesimplypassittotheExpressnextcallbacksoitwillbehandledbyourglobalerrorhandler.
Althoughthesechangesarestraightforward,theyhaveintroducedsomerepetitiveboilerplatetoourcode.Thisisevenmoreofaproblemintheindexroute;takealookatthecodefromsrc/routes/index.js:
varexpress=require('express');
varrouter=express.Router();
vargames=require('../service/games.js');
router.get('/',function(req,res,next){
games.createdBy(req.user.id,(err,createdGames)=>{
if(err){
next(err);
}else{
games.availableTo(req.user.id,(err,availableGames)=>{
if(err){
next(err);
}else{
res.render('index',{
title:'Hangman',
userId:req.user.id,
createdGames:createdGames,
availableGames:availableGames,
partials:{createdGame:'createdGame'}
});
}
});
}
});});
module.exports=router;
Here,weneedtocombinetheresultoftwodifferentasynchronouscalls.Thisleadstonestedcallbacks.Wealsohavetorepeattheerror-handlingcodeateachstage.Notealsothatweonlystartthesecondasynchronousoperationafterthefirstonecompletes.Itwouldbebettertostarttheoperationsinparallel.
Recallthat,whileJavaScriptitselfissingle-threaded,asynchronousoperationsmayperformworkinparallel,forexample,network,disk,andotherI/Ooperations.Runningmultipleoperationsinparallelwouldneedevenmorecomplicated(anderror-prone)boilerplatecode.Foranexampleofhowthismightwork,considerthechangestomakethebeforeEachfunctioninthegamesservicetestasynchronous.Thefollowingcodeisfromsrc/test/services/games.js:
describe('Gameservice',function(){
letfirstUserId='user-id-1';
letsecondUserId='user-id-2';
beforeEach(function(done){
service.availableTo('not-a-user',(err,gamesAdded)=>{
letgamesDeleted=0;
if(gamesAdded.length===0){
done();
}
gamesAdded.forEach(game=>{
game.remove(()=>{
if(++gamesDeleted===gamesAdded.length){
done();
}
});
});
});
});
...
});
Here,weneedtomakeanunknownnumberofcallstotheasynchronousremovemethod.Thedonecallbackmustbeinvokedwhentheyareallcomplete.Thereareseveralwaysofachievingthis,buttheyallinvolveadditionalboilerplate.Theapproachhereisthesimplestpossible,
keepingcountofthenumberofcompleteoperations.Alsonotethatweareomittingerrorhandling,sincethisistestcode.Inproductioncode,wewouldhavetoworryabouterrorhandlingaswell,makingthingsevenmorecomplicated.
Note
Thereareotherchangestotheteststomakeuseofthenewasynchronousinterfaceofthegamesservice.Theyareexcludedhereforbrevity.Theyaresimilartothechangesinindex.js.Youcanseeafullsetofchangesbyviewingthischapter'sfirstcommitintheGitrepositoryathttps://github.com/NodeJsForDevelopers/chapter08.
Thisallseemsquiteunsatisfactory.Ourcodehasbecomemorecomplicated,repetitive,andhardertoread.Fortunately,wecanaddresstheseissuesbyusingadifferentapproachtowritingasynchronouscode.
WritingcleanerasynchronouscodeusingpromisesPromisesareanalternativepatterntocallbacksforwritingasynchronouscode.Apromiserepresentsanoperationthathasn'tcompletedyetbutisexpectedtodosointhefuture.Asthenamepromisesuggests,apromiseisacontracttoeventuallyprovideavalueorareasonforfailure(thatis,anerror).YoumayalreadybefamiliarwiththispatternfromTasksin.NETorFuturesinJava.Apromisehasthreepossiblestates:
pendingrepresentsanin-progressoperationfulfilledrepresentingasuccessfuloperation,witharesultvaluerejectedrepresentinganunsuccessfuloperation,withafailurereason
Whenexecutingasingleoperation,thecallback-basedandpromise-basedapproachesappearquitesimilar.Thepowerofpromisescomeswhencombiningasynchronousoperations.
Consideranexamplewherewehaveasynchronouslibraryfunctionsforobtaining,processing,andaggregatingdata.Wewanttoperformtheseoperationsinturnthendisplaytheresult,handlingerrorsaswego.Usingcallbacks,itmightlooklikethis(innon-runnable,fictionalcode):
lib.getInitialData(function(e,initialData){
if(e){
console.log('Error:'+e);
}else{
lib.processData(initialData,(e,processedData)=>{
if(e){
console.log('Error:'+e);
}else{
lib.aggregateData(processedData,(e,aggregatedData)=>{
if(e){
console.log('Error:'+e);
}else{
console.log('Success!Result='+aggregatedData);
}
});
}
});
}
});
Thishasmanyofthesameproblemsweencounteredinourowncodeintheprevioussection:nestedcallbacks,extraboilerplate,andrepetitiveerror-handling.Ifthesefunctionsinsteadreturnedpromises,theequivalentoftheabovecodewouldbeasfollows:
lib.getInitialData()
.then(lib.processData)
.then(lib.aggregateData)
.then(function(aggregatedData){
console.log('Success!Result='+result);
},function(error){
console.log('Error:'+error);
});
Thethenfunctionappliesafunctiontotheresultingvalueofapromise,returninganewpromise.Inthisway,weconstructachainofpromisesrepresentingaseriesofoperations.
Thethenfunctiontakestwoarguments,whicharebothcallbacks.Iftheasynchronousoperationreturnsanerror,thesecondargumentwillbeinvokedinstead.Intheaboveexample,ifthelibrary.aggregateDatacallfails,thenwewillloganerror.
Ifthesecondthencallbackparameterisomitted,anyerrorspropagatealongthechainofpromises.Intheaboveexample,thismeansthatifthelibrary.processDatacallfails,thenlibrary.aggregateDatawillnotbecalledandourerror-loggingcallbackwillstillbeinvoked.
Ifyouonlycareabouttheerrorcase,youcanjustspecifyanerrorcallbackusingthecatchfunctioninsteadofthen.Youcanalsousethistogetherwithpropagationtorewritetheprecedingcodemoreclearly:
library.getInitialData()
.then(library.processData)
.then(library.aggregateData)
.then(function(aggregatedData){
console.log('Success!Result='+result);
})
.catch(function(error){
console.log('Error:'+error);
});
Here,errorsatanypointpropagatetoafinalpromisewhichwecheckforerrors.Notethatthisrewrittenversionwouldalsocatchanyerrorsthrownbyoursuccess-loggingcallback,whichtheprecedingversionwouldnothavedone.Youshouldalwayscallcatchattheendofapromisechain,unlessyouarereturningtheresultingpromiseobjecttobeconsumedelsewhere.
Implementingpromise-basedasynchronouscodeLet'sapplythepromisepatterntoourexistingapplication.First,we'llneedtoupdateourgameserviceAPItoexposepromisesinsteadofcallbacks.Asbefore,thisisstraightforwardsinceourgameservicedoesn'tactuallyuseanyasynchronousoperationsinitsimplementation(yet).Apromised-basedversionofourgamesservicelookslikethefollowing(insrc/services/games.js):
'usestrict';
constgames=[];
letcurrentId=1;
classGame{
...
remove(){
games.splice(games.indexOf(this),1);
returnPromise.resolve();
}
}
module.exports.create=(userId,word)=>{
constnewGame=newGame(nextId++,userId,word);
games.push(newGame);
returnPromise.resolve(newGame);
};
module.exports.get=(id)=>
Promise.resolve(
games.find(game=>game.id===parseInt(id,10)));
module.exports.createdBy=(userId)=>
Promise.resolve(games.filter(game=>game.setBy===userId));
module.exports.availableTo=(userId)=>
Promise.resolve(games.filter(game=>game.setBy!==userId));
Creatingapromise-basedinterfaceisevensimplerthanacallback-basedone.WecancreateapromiseforanalreadyknownvalueusingthePromise.resolve()function.Eachfunctioninourgamesservicelooksmuchliketheoriginalsynchronousversion,justwithanextracalltoPromise.resolve.
Note
IfyoupassapromiseargumenttoPromise.resolve,thenyougetbackapromisethatbehavesliketheoriginalargument.Ifyoupassanyothervalue,yougetanalreadyresolvedpromiseforthatvalue.Thiscanbeusefulifyouneedtooperateonavariablethatmightbeapromiseoravalue.YoucanpassittoPromise.resolve,thentreatitconsistentlyasapromise.
Consumingthepromisepattern
Nowweneedtoupdatetherestofourcodebasetousepromises.Let'slookthroughthesamefilesasbefore,startingwiththegamesroute.Seethefollowingcodefromsrc/routes/games.js:
'usestrict';
constexpress=require('express');
constrouter=express.Router();
constservice=require('../service/games.js');
router.post('/',function(req,res,next){
letword=req.body.word;
if(word&&/^[A-Za-z]{3,}$/.test(word)){
service.create(req.user.id,word)
.then(game=>
res.redirect(`/games/${game.id}/created`))
.catch(next);
}else{
res.status(400).send('Wordmustbeatleastthreecharacterslongand
containonlyletters');
}
});
constcheckGameExists=function(id,res,onSuccess,onError){
service.get(id)
.then(game=>{
if(game){
onSuccess(game);
}else{
res.status(404).send('Non-existentgameID');
}
})
.catch(onError);
};
...
router.delete('/:id',function(req,res,next){
checkGameExists(
req.params.id,
res,
game=>{
if(game.setBy===req.user.id){
game.remove()
.then(()=>res.send())
.catch(next);
}else{
res.status(403).send('Youdonothavepermissiontodelete
thisgame');
}
},
next);
});
Thisfilewasthesimplestbefore,soshowstheleastdifferencehere.Westillhavealittlerepetitionofboilerplate(forexample,thecatchcall).Still,thepromise-basedapproachismorecompactandreadablethanwithcallbacks.Nowlet'slookattheindexroutecodefromsrc/routes/index.js:
varexpress=require('express');
varrouter=express.Router();
vargames=require('../service/games.js');
router.get('/',function(req,res,next){
games.createdBy(req.user.id)
.then(gamesCreatedByUser=>
games.availableTo(req.user.id)
.then(gamesAvailableToUser=>{
res.render('index',{
title:'Hangman',
userId:req.user.id,
createdGames:gamesCreatedByUser,
availableGames:gamesAvailableToUser
});
}))
.catch(next);
});
module.exports=router;
Thisisalittlebetter.Thereislessrepetition,butstillsomenestingandboilerplate.Notethattheoutermostthencallbackreturnsapromise(chainedfromgames.availableTo).Whenathencallbackreturnsapromise,thisiseffectivelyflattened,sotheoverallpromisereturnsthevalueoftheinnerpromise.Thisflatteningalsoappliestothepropagationoferrors,sowedon'tneedtocallcatchontheinnerpromiseexplicitly.
Thiscodeisstillalittleconfusingtofollow.Thereisactuallyawaytomakeitmuchmorereadable,whichwe'llcomebacktoshortly.Let'sfirstlookatthebeforeEachfunctioninthegamesservicetestinthefollowingcodefromtest/service/games.js:
describe('Gameservice',function(){
letfirstUserId='user-id-1';
letsecondUserId='user-id-2';
beforeEach(function(done){
service.availableTo('non-existent-user')
.then(games=>games.map(game=>game.remove()))
.then(gamesRemoved=>Promise.all(gamesRemoved))
.then(()=>done(),done);
});
});
Thishasbecomemuchshorterandmorelinear.Let'sbreakdownwhateachlinedoes:
service.availableToreturnsapromiseofanarrayofgamesThefirstthencallbackusesarray.maptoconvertthisintoapromiseofanarrayofpromisesofdeleteoperationsThenextthencallbackusesPromise.alltoconvertthisintoasinglepromiseforthewholearrayofdeleteoperations
Note
ThePromise.allfunctiontakesanarrayofpromisesandreturnsapromisethatresolveswhenallofthepromisesinthearrayhaveresolvedorisrejectedassoonasanypromiseinthearrayisrejected.
ThefinalthencallbackisinvokedwhenthepromisereturnedfromPromise.allresolves,thatis,whenallthedeleteoperationsarecomplete,andinvokesMocha'sdonecallback
Notethatunlikewiththecallback-basedapproach,itisalsotrivialtoimplementerrorhandling.Wejustpassinthedonecallbackastheerrorhandler(secondargument)tothefinalthencall.Wecantakeasimilarapproachintheteststhemselvesaswe'vedoneherewiththebeforeEachcallback.Again,theupdatestothetestsareomittedforbrevity,butyoucanfindtheminthebook'scompanioncode.
ParallelisingoperationsusingpromisesWecanalsomakeuseofthePromise.allfunctiontosimplifytheindexroute.Recallthatourcodeisinvokingthetwoasynchronousoperationsoneaftertheother.Inthecallback-basedapproach,attemptingtoexecutetheseinparallelwouldhavemadethecodeevenmorecomplicated.Withpromises,itactuallymakesourcodemorereadable:
varexpress=require('express');
varrouter=express.Router();
vargames=require('../service/games.js');
router.get('/',function(req,res,next){
Promise.all([
games.createdBy(req.user.id),
games.availableTo(req.user.id)
])
.then(results=>{
res.render('index',{
title:'Hangman',
userId:req.user.id,
createdGames:results[0],
availableGames:results[1]
});
})
.catch(next);
});
module.exports=router;
Thisisshorterandmucheasiertounderstand.Wekickofftwoasynchronousoperationstoloaddata,thenmakeuseofthedataassoonasbothoperationshavecompleted.
Tip
Theonlyslightdrawbackoftheprecedingapproachisthatwehavetogeteachofthetwovaluesbackoutofthearraybytheirindex.InNode.jsv6orhigher,wecouldavoidthisandmakethecodemorereadablestillbyusingdestructuringtoassigntwonamedparametersfromthevaluesinthearray,asfollows:
.then(([created,available])=>{...
Thisisn'tusedintheexampleaboveforback-compatibilitywithNode.jsv4.WewilldiscussdestructuringinmoredetailinChapter14,Node.jsandBeyond.
CombiningasynchronousprogrammingpatternsPromisesallowustoaddresssomeoftheshortcomingsofthecallbackpatternandwritemorereadablecode.Nowwehaveanewproblem,though.OneofthemeritsofNode.jsistheconsistentapproachtoasynchronousprogramming.Weseemtohavenegatedthisbyintroducingpromisesaswellastheconventionalcallbackpattern.
Furthermore,althoughnativepromisesarenewtoECMAScript2015,theconceptisnotnew.Therearemanypre-existinglibrariesthatprovidetheirownimplementationofpromises.
Fortunately,thesecompetingapproachestoasynchronousprogrammingareactuallyveryconsistent.ThebiggestvalueoftheconsistencyintheNode.js-stylecallbackpatterncomesfromthefollowing:
Alllibraryfunctionsareasynchronous(non-blocking)bydefaultAllasynchronousoperationsreturnasinglevalueoranerror
Promisesarecompletelyconsistentwiththeabovepoints.ThereisalsoexcellentcompatibilitybetweendifferentimplementationsofpromisesinJavaScript.ThisisthankstothePromises/A+specification(http://promisesaplus.com).Thisessentiallydefinesthebehaviorofthethenmethod.Anypromiselibraryyouarelikelytocomeacrosswillfollowthisspec.NativeJavaScriptpromisesarealsodesignedtobecompatiblewithit.ThesemeansthatalloftheselibrariesandnativeJavaScriptpromisesareinteroperable.
Soalllibrariesusingcallbacksfollowthesameconventionandallpromiselibrariesfollowthesamespecification.Theonlyissueremainingisconvertingbetweenpromisesandcallbacks.Thereareseveralpromiselibrariesthatcandothisforus.
Ifyoujustwanttoconvertafewstandardcallbackfunctionstopromises,youcanusedenodeify,whichcanbeinstalledusingnpm.Ourfs.statexamplefromearlierwouldlooklikethis:
constdenodeify=require('denodeify');
conststat=denodeify(require('fs').stat));
stat('/hello/world')
.then(stats=>console.log('Filelastupdatedat:'+stats.mtime));
Youwillalsofindthatmanylibrariesexposefunctionsthatcanreturnapromiseoracceptacallbackandsocanbeinvokedwitheitherpattern.
SummaryInthischapter,wehaveseenhowtoexposethestandardNode.jscallbackinterfaceinourownmodules.Wehavemadeuseofpromisestoproducemorereadableasynchronouscode.Finally,wehaveseenhowwecanusepromisestogetherwithstandardNode.jscallbacks.
NowthatwecanimplementourownasynchronousAPIs,wecanexpandonourapplicationandstartmakinguseofotherlibrariesthatprovideasynchronousinterfaces.Inthenextchapter,wewillmakeuseofthistointroducepersistentstoragetoourapplication.
Chapter9.PersistingDataMostapplicationsneedtopersistsomekindofdata.Inthischapter,we'llbelookingatsomeapproachestodatapersistenceforNode.jsapplications.
Thedefaultchoiceforpersistenceforalongtimehasbeenthetraditionalrelationaldatabase.YoumayhaveusedRDBMSs(relationaldatabasemanagementsystems)suchasMicrosoftSQLServer,Oracle,MySQLorPostgreSQL.ThesesystemsareoftencategorizedasSQLdatabasessincetheyalluseSQLastheirprimaryquerylanguage.
Morerecently,therehasbeenaproliferationofso-calledNoSQLdatabases.Thisumbrellatermisn'tparticularlyusefulasacategory.SomeNoSQLdatabaseshavenomoreincommonwitheachotherthanwithtraditionalrelationaldatabases.
What'sinterestingistherangeofdatabasesavailableandtheusecasestheyfulfil.TraditionalRDBMSsareaspowerfulandflexibleaseverandtherightchoiceformanysituations.Inthischapter,we'llconsidertwoothertypesofdatabase,alongwithhowandwhentomakeuseofthem.
Thesystemswe'llbelookingatareMongoDBandRedis.Bothofthesehadtheirinitialreleasein2009andarenowwidely-used.Coveringeitherofthemindepthwouldjustifyabookinitself.Theaimofthischapteristoprovideanintroductiontoandhigh-leveloverviewofeach.
Inthischapter,wewillcoverthefollowingtopics:
TheconceptualdatamodelusedbyeachofthesesystemsTheusecasesforwhichtheyprovidethemostbenefitIntegratingthemwithanExpressapplicationTestingdatapersistencecode
IntroducingMongoDBMongoDBisadocument-orientedDBMS.MongoDBdocumentsarestoredasbinaryJSON(BSON).ThisissimilartoJSON,butwithsupportforadditionaldatatypes.JSONfieldvaluescanonlybestrings,numbers,objects,arrays,Booleans,ornull.BSONsupportsmorespecificnumerictypes,datesandtimestamps,regularexpressions,andbinarydata.Asthenamesuggests,BSONdocumentsarestoredandtransferredasbinarydata.ThiscanbemoreefficientthanJSON'sstringrepresentation.
MongoDBdocumentsarestoredincollections.Theseworkverymuchliketablesinatraditionalrelationaldatabase.Documentscanbeinserted,updated,andqueried.Therearetwokeydifferencesfromatraditionalrelationaldatabase:
MongoDBdoesnotsupportserver-sidejoins.InatraditionalRDBMS,youwouldnormalizedataintomultipletablesandjoinacrossthemusingforeignkeys.InMongoDB,youinsteaduseBSON'snestedstructuretodenormalizedataabouteachentityintoasingledocument.Therelationalpropertyofarelationaldatabaseisthatallrowsinatablecontainthesamefieldswiththesamemeaning.InMongoDB,documentscanhaveanysetoffields.
Inpractice,documentsinthesamecollectiontypicallyhavethesamefieldsoratleastacommoncoresetoffields.MongoDBsupportsthecreationofindexesoncommonfieldsinacollectiontomakequeryingmoreefficient.
WhychooseMongoDB?ThereareseveralpropertiesofMongoDBthatmakeitanappealingchoiceforsomeusecases,especiallyinNode.js-basedapplications.We'llcovertheseinthissection.
Objectmodeling
MongoDB'sdocument-basedapproachcanbeagoodfitforpersistingdomainentities.YoumayhaveexperienceofstoringdomainentitiesinarelationaldatabaseusinganObject-RelationalMapper(ORM).HibernateandEntityFrameworkarepopularexamplesofORMs.OneofthejobsperformedbyanORMismappingasingleentitytomultipletablesinanormalizedschema.Whenanentityisloadedfromthedatabase,itisreconstructedviaJOINqueriesbetweenthesetables.ThisisoneofthekeyfeaturesofORMs.ItisalsooneofthemostcommonsourcesofconfigurationproblemsandperformanceissueswhenusinganORM.MongoDBpersistseachentityasasingledocument,whichcanbemuchsimpler.
Ofcourse,cross-tablejoinscanalsobeusefulfortraversingrelationshipsbetweenentities.WhileORMstypicallymakethiseasy,thiscanitselfbeasourceofperformanceproblems.ImplicitloadingofrelatedentitiesoftencausesN+1problems,issuingthousandsofDBqueries.Handlingtheserelationshipswellrequirescarefulthought,whateverkindofdatabaseyouareusing.
WhenusinganORMandanRDBMS,allinter-entityrelationshipsareforeignkeys,butyouneedtothinkcarefullyabouthowtoloadthem.WhenmodelingdatainMongoDB,youmustchoosebetweenembeddeddocumentsordocumentreferencesforinter-entityrelationships.Undereithertechstack,thedesigndecisionsdependonthedataaccessrequirementsofyourapplicationanddesigningthedatamodeltoreducetheprevalenceofinter-entityrelationshipswillsimplifymatters.
JavaScript
MongoDBisagoodfitforNode.jsinparticular.TheuseofaJSON-likeformatmapswelltoaJavaScript-basedprogrammingenvironment.MongoDBitselfalsorunsJavaScriptnatively.DatabaseoperationscanmakeuseofcustomJavaScriptfunctionsthatexecuteontheserver.
Scalability
MongoDBalsoscalesinasimilarmannertoNode.js.Itusespartitioningandreplicationtosupporthorizontalscalingoncommodityhardware.Thereisnotechnicalreasonwhyyourapplicationanddatabasehavetoscaleinthesameway,butitmaybeeasiertoplanforscalabilityfromabusinessperspective.
WhenusinganRDBMS,itismorestraightforwardtoscalethedatabasevertically.Thatmeansprovisioningahigh-powereddatabaseserverthatcansupportmultipleapplicationservers.Thisrequiresmorecarefulplanningandmoreup-frontinvestmentthanlinearlyscalingapplicationanddatabaseservershorizontallytogether.
GettingstartedwithMongoDBVisithttps://www.mongodb.org/downloadstodownloadandinstallthelatestversionoftheMongoDBCommunityServereditionforyouroperatingsystem.Therearemoredetailedinstallationinstructionsintheusermanualathttps://docs.mongodb.org/manual/installation/.
ThecommandsintherestofthissectionmakeuseofexecutablesinMongoDB's/bindirectory.Youcanrunthecommandsinthisdirectoryor,betterstill,addittoyourPATH.
CreateadirectoryforMongoDBtostoreitsdata.ThenstarttheMongoDBdaemonprocess(thatis,service),providingthepathofthatdirectoryasfollows:
>mongod--dbpathC:\data\mongodb
UsingtheMongoDBshell
YoucaninteractwithMongoDBfromtheconsoleusingitsbuilt-inshellapplication.YoucanlaunchtheMongoDBshellbyrunningthemongocommand,asfollows:
>mongodemo
Thiswillconnecttoadatabasenameddemo(creatingit,ifnecessary)onthelocalserver.Ifyoudon'tspecifyadatabase,thentheshellconnectstoadatabasenamedtest.
ThefirstthingtonoticeisthattheshellisjustanotherJavaScriptenvironment.WecantryrunningsomeofthesamecommandsasatthebeginningofChapter2,GettingStartedwithNode.js.
>functionsquare(x){returnx*x;}
>square(42)
1764
>newDate()
ISODate("2016-01-01T20:05:39.652Z")
>varfoo={bar:"baz"}
>typeoffoo
object
>foo.bar
baz
JustasNode.jsbuildsonJavaScriptinwaysthatmakeitmoresuitableforserver-sideapplicationdevelopment,MongoDBaddsfeaturesmoreusefultodatapersistence.NotethatnewDate()intheprecedingcodereturnsanISODate,MongoDB'sstandarddatatypeforrepresentingdatesinBSONdocuments.
Youcanquittheconsolebytypingexitatanytime.
MongoDBalsoaddssomenewglobalvariablesforinteractingwiththedatabase.Themostimportantoftheseisthedbobject.Let'stryaddingsomedocumentstoourdatabase.RecallthatMongoDBstoresdocumentsincollections.Tocreateanewcollection,wejustneedtostartinsertingdocumentsintoit.Forasimpleexample,we'llusetheUKbankholidaysfor2016.We
canpopulatethiscollectionusingthefollowingscript:
db.holidays.insert(
{name:"NewYear'sDay",date:ISODate("2016-01-01")});
db.holidays.insert(
{name:"GoodFriday",date:ISODate("2016-03-25")});
db.holidays.insert(
{name:"EasterMonday",date:ISODate("2016-03-28")});
db.holidays.insert(
{name:"EarlyMaybankholiday",date:ISODate("2016-05-02")});
db.holidays.insert(
{name:"Springbankholiday",date:ISODate("2016-05-30")});
db.holidays.insert(
{name:"Summerbankholiday",date:ISODate("2016-08-29")});
db.holidays.insert(
{name:"BoxingDay",date:ISODate("2016-12-26")});
db.holidays.insert(
{name:"ChristmasDay",date:ISODate("2016-12-27"),
substitute_for:ISODate("2016-12-25")});
NotethatChristmasDayfallsonaSundayin2016,sothebankholidayoccursonthenextworkingday.Thisgivesusareasontohaveanotherfieldthatisonlyrelevanttosomedocumentsinthecollection.
Youcouldtypetheseinsertcommandsintotheconsolemanually,butit'seasiertotellMongoDBtoloadthemfromascriptfile:
>mongodemoholidays.js--shell
Thepreviouscommandconnectstoadatabasenameddemo,runstheholiday.jsscript(availableinthebook'scompanioncode),thenopensashelltoallowustointeractwiththedatabase.WecanviewthecompletecontentsofthecollectionbyrunningthefollowingcommandintheMongoDBconsole:
>db.holidays.find()
{"_id":ObjectId("572f760fffb6888d70c45eeb"),"name":"NewYear'sDay",
"date":ISODate("2016-01-01T00:00:00Z")}
{"_id":ObjectId("572f7610ffb6888d70c45eec"),"name":"GoodFriday",
"date":ISODate("2016-03-25T00:00:00Z")}
...
NotethatMongoDBhasautomaticallyaddedan_idfieldtoeachdocumentforus.
Tip
YoucanseehowMongoDBdoesthisbyviewingthesourceoftheinsertmethod.Justtypedb.holidays.insertintotheshell(withnoparentheses).
Wecanpulloutrecordsbytheir_idorothersinglefields:
>db.holidays.find({name:"BoxingDay"})
Thiswillreturnanyobjectsthatmatchtheobjectpassedtofind.Tolookupdocumentsbysomethingotherthanexactequality,wecanuseMongoDB'squeryoperators.Theseareprefixedwiththedollarsymbolandspecifiedasobjectproperties.Forexample,tofindholidaysinthesecondhalfoftheyear,wecanusethegreaterthanorequaltooperatorasfollows:
>db.holidays.find({date:{$gte:newDate("2016-07-01")}})
MongoDB'saggregationpipelineallowsustobuildcomplexqueriesfromasequenceofoperationscalledpipelinestages.ItistheclosestthinginMongoDBtocomplexqueryinginSQL.Here,wecountthenumberofbankholidaysineachmonthusingMongoDB's$grouppipelinestage,whichissimilartoSQL'sGROUPBYclause:
>db.holidays.aggregate({
$group:{_id:{$month:"$date"},count:{$sum:1}}})
Anoddquirkofthecalendarin2016meansthattheChristmasDayBankHolidayactuallycomesafterBoxingDay(sinceChristmasDayitselfisonaSunday).Inthefollowingexample,weorderbankholidaysbythedateoftheoccasionthattheymark(storedinthe$substitute_forfieldifdifferentfromthedateofthebankholiday):
>db.holidays.aggregate([
{$project:{_id:false,name:"$name",
date:{$ifNull:["$substitute_for","$date"]}}},
{$sort:{date:1}}
])
Thepreviouspipelineconsistsoftwostages:
The$projectstagespecifiesasetoffieldsbasedontheunderlyingdata(similartoSELECTinSQL).Notethatthe_idfieldisincludedbydefault,butweexcludeithere.The$sortstagespecifiesasortfieldanddirection(similartoSQL'sSORTBYclause).The1hereindicatesanascendingsortorder.
Wehavejustscratchedthesurfacehere.TherearemanymorepipelinephasesavailableinMongoDB.YoucanfindoutmoreaboutaggregationintheMongoDBdocumentationathttps://docs.mongodb.com/manual/core/aggregation-pipeline/.
MongoDBalsohasabuilt-inMap-ReducefunctionforpowerfulaggregatedataprocessingusingarbitraryJavaScriptfunctions.Thisisbeyondthescopeofthisbook,butyoucanfindoutmoreaboutMap-ReduceandMongoDB'simplementationofitathttps://docs.mongodb.com/manual/core/map-reduce/.
UsingMongoDBwithExpressThegamesservicemoduleinourapplicationcurrentlystoresallitsdatainmemory.Thisworkedwellenoughfordemopurposes,butisn'tsuitableforarealapplication.Weloseallthedatawhenevertheapplicationrestarts.Italsopreventsusfromscalingourapplicationacrossmultipleprocesses.Eachinstancewouldhaveitsowngameservicewithdifferentdata.Userswouldseedifferentdatadependingonwhichserverhappenedtohandletheirrequest.
We'regoingtoupdateourgamesservicetostoreitsdatainMongoDB.Forthis,we'regoingtomakeuseofalibrarycalledMongoose.
PersistingobjectswithMongooseRecallthat,unlikearelationaldatabase,MongoDBdoesnotrequiredocumentsinthesamecollectiontohavethesamefields.However,wedotypicallyexpectmostitemswithinacollectiontoshareatleastacommoncoreoffields.
MongooseisanobjectmodelinglibraryforstoringentitiesinMongoDB.Ithelpswithwritingcommonfunctionalitysuchasvalidation,querybuilding,andtypecasting.Italsoprovideshooksforassociatingbusinesslogicwithourentities.ThesearesimilartosomeofthefeaturesprovidedbyORMssuchasEntityFrameworkorHibernate.MongooseitselfisnotanORM,though.Recallthatobject-relationalmappingisnotrelevantfordocument-orienteddatabasessuchasMongoDB.
TouseMongoose,westartbydefiningaschema.ThisdefinesthecommonfieldsfordocumentswithinaMongoDBcollection.Returningtoourdemoapplicationfromtheprecedingchapters,let'sinstallMongooseanddefineaschemaforourgamescollection:
>npminstallmongoose--save
Thefollowingcodeisaddedtosrc/services/games.js:
'usestrict';
constmongoose=require('mongoose');
constSchema=mongoose.Schema;
constgameSchema=newSchema({
word:String,
setBy:String
});
Theschemadefinesdocumentfieldsandspecifiesthetypeofeachfield.Tostartpersistingdocumentswiththisschema,weneedtocreateamodel.
ModelsareconstructorsthatcorrespondtoaMongoDBcollection.InstancesofaMongoosemodelcorrespondtodocumentsinthatcollection.Modelsalsoprovidefunctionsformodifyingthecollection.Wecreateamodelbyspecifyingtheschemaand(singular)collectionname:
constgameSchema=newSchema({
word:String,
setBy:String
});
constGame=mongoose.model('Game',gameSchema);
TheModelconstructorreplacesourGameclassandconstructorfrombefore.Thisclassalsocontainedtwoinstancemethods:positionsOfandremove.Wecandefinecustominstancemethodsonaschema,whichwillbeavailableonallmodelinstances.Thesemustbedefinedbeforecreatingthemodel:
constgameSchema=newSchema({
word:String,
setBy:String
});
gameSchema.methods.positionsOf=function(character){
letpositions=[];
for(letiinthis.word){
if(this.word[i]===character.toUpperCase()){
positions.push(i);
}
}
returnpositions;
};
constGame=mongoose.model('Game',gameSchema);
Note
Notethatweuseatraditionalfunctiondefinitionratherthananarrowfunctionintheprecedingcode.Thisisnecessaryinorderforthethiskeywordinsidethefunctiontoworkcorrectly.Seehttp://derickbailey.com/2015/09/28/do-es6-arrow-functions-really-solve-this-in-javascript/formoredetails.
Wedon'tneedtodefinearemovemethodanymore,becauseMongooseprovidesthisautomatically.Italsoprovidesasavemethod,whichwecanuseforpersistingnewgames:
constGame=mongoose.model('Game',gameSchema);
module.exports.create=(userId,word)=>{
letgame=newGame({setBy:userId,word:word.toUpperCase()});
returngame.save();
};
Wedon'tneedtospecifyanIDanymore,sincethisisalsoprovidedbyMongoose.Notethatwedoneedtospecifyword.toUpperCase(),whichusedtobeintheGameconstructor.Thisisn'taproblem,sincetheconstructorisprivatetoourmodule.Nocodeoutsidethemodulecaninvoketheconstructordirectly.WherethetoUpperCasecalltakesplaceisjustanimplementationdetail.
AlsonotethatMongoose'sasyncoperationsallreturnpromisesasanalternativetousingcallbacks.Mongoosesupportsbothoftheasynchronousprogrammingpatternsdiscussedinthepreviouschapter.Mongooseusesitsownimplementationofpromises.WecanconfigureMongoosetouseECMAScript6promises,though.WealsoneedtotellMongoosetoconnecttoaMongoDBdatabase.Fornow,wewillhardcodetheURL,butwe'llseehowtomakethisconfigurableshortly:
constmongoose=require('mongoose');
mongoose.Promise=Promise;
mongoose.connect('mongodb://localhost/hangman');
Finally,weneedtoimplementourthreemethodsforretrievinggamesfromthedatabase.Wecan
dothisusingMongoose'sfindmethod:
module.exports.create=(userId,word)=>{
...
};
module.exports.createdBy=
(userId)=>Game.find({setBy:userId});
module.exports.availableTo=
(userId)=>Game.find({setBy:{$ne:userId}});
module.exports.get=
(id)=>Game.findById(id);
TheMongoosefindmethodworksliketheMongoDBfindmethodwesawintheprevioussection,UsingtheMongoDBshell.IttakesasetofMongoDBqueryconditionsandasynchronouslyprovidesalistofdocuments.findByIdtakesanIDandasynchronouslyprovidesasingledocument,ornull.
Mongoosealsoprovidesawheremethodforbuildingupconditionsthroughfunctioncalls.TheavailableTofunctioncanberewrittenasfollows:
module.exports.availableTo=
(userId)=>Game.where('setBy').ne(userId);
AslongasyoustillhaveMongoDBrunninglocally(asdescribedinGettingstartedwithMongoDBearlierinthechapter),youshouldnowbeabletoruntheapplication.Trystoppingandrestartingtheapplicationandnoticethatgamesarenowpersistedbetweenrestarts.
IsolatingpersistencecodeIt'susefultointegratewitharealdatabasetomakesureourpersistencecodeisworking.Butit'snotalwaysappropriateforourteststobedependentonanexternalMongoDBinstance.
Wewantdeveloperstobeabletocheckoutthecodeandruntheapplicationwithoutneedingtorunadatabaseinstance.Also,externaldependenciesslowdownourtests.MongoDBstoresdataondisk,sointroducesadditionalI/Oworkintoourtests.
Theapplicationshoulddependonanexternaldatabaseinproduction.Inintegration,wewanttousearealdatabaseonthelocalserver.Ondevelopmentmachines,itwouldbebettertouseanin-memorydatabasebydefault.SoweneedtobeabletoconfigureadatabaseURLandfallbacktoanin-memorydatabaseindevelopmentenvironments.
Finally,weneedtoinitializeMongoosebeforeusingitinourgamesservice.ThisincludesspecifyingthedatabaseURLandwaitingforaconnectiontobeestablished.Thishappensasynchronously,socan'tbepartofthegamesservicemoduledefinition.Wealsodon'twantclientsofthegamesservicetohavetopassinaMongooseinstancetoeachfunctioncall.
Wecanaddressalloftheseissuesbyintroducingdependencyinjectiontoourapplication.We'llpassinthegameserviceasadependencytothemodulesthatneeditandpassinMongooseasadependencytothegamesservice.
Tip
Thiswouldalsogiveustheoptionofwritingunittestsforothermodulesthatpassinatestdoubleforthegamesserviceitself,sodon'tuseMongoDBatall.Inlargerapplications,thiskindoftestisolationisimportantforwritingfastandmaintainabletests.
DependencyinjectioninNode.jsYoumayhaveuseddependencyinjection(DI)frameworkssuchasUnity,Autofac,NInject,orSpringin.NETorJava.Theseprovidefeaturessuchasdeclarativeconfigurationandautowiringofdependencies.TherearesimilarDIcontainersavailableforJavaScript.However,itismorecommontopassarounddependenciesexplicitly.JavaScript'smodulepatternmakesthisapproachmorenaturalthaninotherlanguages.Wedon'tneedtoaddalotoffieldsandconstructors/propertiestosetupdependencies.Wecanjustwrapmodulesinaninitializationfunctionthattakesdependenciesasparameters.
Inourapplication,theappmodulewillwireeverythingtogether.Theapplicationasawholedependsonthedatabase.Thegamesandindexroutesdependonthegameservice.Toallowtheroutestotakeadependencyonthegameservice,wejustneedtotopandtailthemwithafunction:
'usestrict';
module.exports=(gamesService)=>{
varexpress=require('express');
varrouter=express.Router();
...
returnrouter;
};
Thegamesserviceitselfisslightlymorecomplicated.Wepreviouslyaddedseveralfunctionstomodule.exports,soweneedtoputtheseonanobjectinstead.However,thisactuallyresultsinshortercode.Also,notethatweonlycreatetheGameschemaifithasn'talreadybeendefined,todefendagainstourexportedfunctionbeingcalledmultipletimes:
module.exports=(mongoose)=>{
'usestrict';
letGame=mongoose.models['Game'];
if(!Game){
constSchema=mongoose.Schema;
constgameSchema=newSchema({
word:String,
setBy:String
});
gameSchema.methods.positionsOf=function(character){
...
};
Game=mongoose.model('Game',gameSchema);
}
return{
create:(userId,word)=>{
constgame=newGame({
setBy:userId,word:word.toUpperCase()
});
returngame.save();
},
createdBy:userId=>Game.find({setBy:userId}),
availableTo:userId=>Game.where('setBy').ne(userId),
get:id=>Game.findById(id)
};
};
Finally,theapplicationitselfdependsonthedatabaseconnectionandwiresuptheotherdependencies:
module.exports=(mongoose)=>{
...
letgamesService=require('./services/games')(mongoose);
letroutes=require('./routes/index')(gamesService);
letgames=require('./routes/games')(gamesService);
...
returnapp;
};
ProvidingdependenciesWecanspecifythedatabaseURLinanenvironmentvariable.Whenthisisn'tpresent,ourapplicationwillinsteadmakeuseofanin-memoryinstanceofMongoDB.ThiswillbeprovidedbyalibrarycalledMockgoose.Weinstallthisasadevdependency,incaseweforgettosetourenvironmentvariableonaproductionserver.We'llgetanerrorratherthanquietlyusinganon-persistentdatabase.
>npminstallmockgoose@~5.x--save-dev
Wecreateanewmoduleundersrc/config/mongoose.jstoinitializeMongooseandreturnapromisethatwillbefulfilledwhenithasconnectedtothedatabase:
'usestrict';
constmongoose=require('mongoose');
constdebug=require('debug')('hangman:config:mongoose');
mongoose.Promise=Promise;
if(!process.env.MONGODB_URL){
debug('MongoDBURLnotfound.Fallingbacktoin-memorydatabase...');
require('mockgoose')(mongoose);
}
letdb=mongoose.connection;
mongoose.connect(process.env.MONGODB_URL);
module.exports=newPromise(function(resolve,reject){
db.once('open',()=>resolve(mongoose));
db.on('error',reject);
});
Nowwejustneedtopassthisintoourapplication.Thefollowingisthecodefrombin/www:
...
require('../src/config/mongoose').then((mongoose)=>{
varapp=require('../src/app')(mongoose);
...
server.on('listening',onListening);
}).catch(function(error){
console.log(error);
process.exit(1);
});
Toallowourteststorun,we'llalsoneedtoaddnewbeforefunctionstomakeuseofthismodule.Thefollowingcodeisfromtest/services/games.js:
'usestrict';
constexpect=require('chai').expect;
describe('Gameservice',()=>{
constfirstUserId='user-id-1';
constsecondUserId='user-id-2';
letservice;
before(done=>{
require('../../src/config/mongoose.js').then((mongoose)=>{
service=require('../../src/services/games.js')(mongoose);
done();
}).catch(done);;
});
...
Thefollowingcodeisfromtest/routes/games.js:
'usestrict';
constrequest=require('supertest');
constexpect=require('chai').expect;
describe('/games',()=>{
letagent,userId;
letmongoose,gamesService,app;
before(function(done){
require('../../src/config/mongoose.js').then((mongoose)=>{
app=require('../../src/app.js')(mongoose);
gamesService=
require('../../src/services/games.js')(mongoose);
done();
}).catch(done);
});
...
We'llalsoaddaglobalteardownfunctiontoclosethedatabaseconnectionafteralltestshavefinished.ThisisjustaMochaafterhookoutsidethecontextofanydescribeblock.Weaddthisinanewfileundertest/global.js:
'usestrict';
after(function(done){
require('../src/config/mongoose.js').then(
(mongoose)=>mongoose.disconnect(done));
});
Finally,weneedtoupdateourgulpfile.js,toallowourintegrationteststorunwiththenewdependency:
gulp.task('integration-test',
['lint-integration-test','test'],function(done){
constTEST_PORT=5000;
require('./src/config/mongoose.js').then((mongoose)=>{
letserver,teardown=(error)=>{
server.close(()=>
mongoose.disconnect(()=>done(error)));
};
server=require('http')
.createServer(require('./src/app.js')(mongoose))
.listen(TEST_PORT,function(){
gulp.src('integration-test/**/*.js')
.pipe(
...
)
.on('error',teardown)
.on('end',teardown)
});
});
});
WecannowrunourapplicationandtestsonalocaldevelopmentmachinewithoutneedingtohaveMongoDBrunning,orwecanspecifytheMONGO_DBenvironmentvariableifandwhenwewanttousearealMongoDBinstance.
RunningdatabaseintegrationtestsonTravisCIWedowanttoregularlyintegrationtestourapplicationagainstarealMongoDBinstance.Fortunately,TravisCIprovidesvariousdatastoresaspartofitsenvironment.WejustneedtotellitthatourbuildrequiresMongoDBbyaddingittoourtravis.ymlfile.WealsoneedtosettheMONGODB_URLenvironmentvariableforteststobeabletoconnecttothedatabase:
services:
-mongodb
env:
global:
-MONGODB_URL=mongodb://localhost/hangman
NowwecanrunourapplicationaswellasourunitandintegrationtestswithasuitableMongoDBinstanceondevelopmentmachinesandontheCIserver.
IntroducingRedisRedisisoftenclassifiedasakey-valuedatastore.Redisdescribesitselfasadata-structurestore.Itoffersstoragetypessimilartothebasicdatastructuresfoundinmostprogramminglanguages.
WhyuseRedis?Redisoperatesentirelyinmemory,allowingittobeveryfast.This,togetherwithitskey-valuenature,makesitwell-suitedforuseasacache.Italsosupportspublish/subscribechannels,whichallowsittofunctionasamessagebroker.We'lllookatthisfurtherinChapter10,Real-timeWebAppsinNode.js.
Moregenerally,RediscanbeausefulbackendtoallowmultipleNode.jsprocessestoco-ordinatewithoneanother.Node.jsscaleshorizontallyandmostwebsiteswillrunmultipleNode.jsprocesses.Manywebsiteshave"working"datathatdoesn'tneedtobepersistedlongterm,butdoesneedtobeavailablequicklyandconsistentlyacrossallprocesses.Redis'sin-memorynatureandrangeofatomicoperationsmakeitveryusefulforthispurpose.
Redisisbuiltmoreforspeedthandurability.Therearevariousoptionstoconfigureit,butallexpectsomeamountofdatalossintheeventofanoutage.ThisisacompromiseofRedisworkingentirelyin-memoryforspeed.Itispossibletoreducedatalosstonomorethanthelastsecondofwritesbeforeanoutage,withoutsignificantlycompromisingonspeed.Rediscanbeconfiguredtocompletelyminimizedatalossbysyncingtodiskaftereachoperation.However,thishasamoresignificantimpactonperformanceandnegatestheadvantagesofRedis'sin-memorynature.
InstallingRedisSourcedistributionsofRedisareavailablefromhttp://redis.io/download.
ForWindows,itismoreusefultodownloadapre-builtbinary.ItisavailableasasignedpackageviaNuGetandChocolatey.IfyouhaveChocolateyavailable,youcaninstallRedisbyrunningthefollowingcommand:
>chocoinstallredis-64
Alternatively,youcandownloadanunsignedversionoftheinstallerfromhttps://github.com/MSOpenTech/redis/releases
Onceinstalled,youcanstartRedisbyrunningredis-server.Inaseparatewindow,runredis-clitoconnecttotheserverandruncommands.
UsingRedisasakey-valuestoreEverythinginRedisisstoredagainstakey.KeysinRediscanbeanybinarydata,butit'sbesttothinkofthemasstrings.Varioustypesofvaluecanbestoredagainsteachkey.
RedisreferstosimplescalarvaluesasStrings.Redisalsohasspecialtreatmentforscalarintegers.Thefollowingexamplesetsandupdatesakeynamedcounter:
127.0.0.1:6379>setcounter100
OK
127.0.0.1:6379>getcounter
"100"
127.0.0.1:6379>incrcounter
(integer)101
Thisincrementoperationisatomic.Redisalsosupportssettingvaluesatomically.Thefollowingcommandwillfailbecausethekeyalreadyexists:
127.0.0.1:6379>setcounter200nx
(nil)
Thesefeaturescanhelpcoordinatingbetweenservers.Redisalsosupportssettingexpirytimesforkeys.Thismakesitpossibletooffercachingbehaviorsimilartomemcache.Redishasevenmoreflexibility,though,aswe'llseeinthenextsection.
StoringstructureddatainRedisInadditiontosimplekey-valuepairs,Redissupportsothermorestructureddatatypes.
Listsareorderedcollectionsofvalues.Theyarestoredasalinkedlistratherthanasarrays.Thismakesadding/removingelementsattheendsofthelistefficient(atthecostofslowerretrievalofitemsfromthelistbyindex),forexample:
127.0.0.1:6379>rpushfruitapplebananapear
(integer)3
127.0.0.1:6379>rpopfruit
"pear"
127.0.0.1:6379>lpushfruitorange
(integer)3
127.0.0.1:6379>lrangefruit0-1
1)"orange"
2)"apple"
3)"banana"
Notethatlrangetakesstartandendindices.Negativevaluescountbackwardsfromtheendofthelist,so-1referstothelastelement.Beingabletopush/popfromeitherendofalistmeansthattheycanbeusedasstacksorqueues,forexample,forallowingprocessestocommunicateinaproducer-consumerarrangement.
Hashesareasetoffield-valuepairs.ThesearenotasrichasMongoDBdocuments,butallowustoassociatesomedatatogether.Forexample,wecouldhaveimplementedourgameserviceusingRedis:
127.0.0.1:6379>hmsetgame:2wordJavaScriptsetByuser-id-7
OK
127.0.0.1:6379>hgetgame:2word
"JavaScript"
127.0.0.1:6379>hgetallgame:2
1)"word"
2)"JavaScript"
3)"setBy"
4)"user-id-7"
Notethatthetop-levelkeygame:2hereisjustaconvention.Itcanbeusefulfordeveloperstonamespacekeysinthisway,butRedisonlyunderstandsthemasstrings.
Setsareunorderedcollectionsofvalues,forexample:
127.0.0.1:6379>saddnumbersonetwothree
(integer)3
127.0.0.1:6379>smembersnumbers
1)"two"
2)"three"
3)"one"
Setssupportmathematicaloperationssuchasunionsandintersections.Theyalsosupportthe
retrieval(withoptionalatomicremoval)ofrandomelements.
Sortedsetsarecollectionsofvalues,eachassociatedwithanumericalscore:
127.0.0.1:6379>zaddvotes3Aye
(integer)1
127.0.0.1:6379>zaddvotes4No
(integer)1
127.0.0.1:6379>zaddvotes1Abstain
(integer)1
127.0.0.1:6379>zrevrangevotes01
1)"No"
2)"Aye"
Notethattherangesareorderedsmallesttolargestbydefault.Werequestareverserangeabovetogettheelementwiththehighestscorefirst.Sortedsetsareusefulforimplementingvotingsystems(aspreviouslyshown)orrankingsystems.
BuildingauserrankingsystemwithRedisWewanttobeabletorankusersbasedonhowmanygamestheyhavecompleted.Wewillcreateauserservice,implementedinRedis,thatprovidesthefollowingfunctionality:
RecordwhenausersuccessfullycompletesagameReturnthetopthreeusersacrossthesiteReturntherankofagivenuser
Wewillfirstaddafeaturetomakethesiteabitmoreuser-friendlybyallowinguserstochooseascreenname.
UsingRedisfromNode.jsFirst,we'llneedtoinstallaNode.jsclientlibraryforRedis.We'llalsousethepromiselibraryBluebirdtoconverttheRedisclientlibrarytopromises:
>npminstallredis--save
>npminstallbluebird--save
First,we'llcreateamoduleforconfiguringtheRedisclientasshownhereinsrc/config/redis.js:
'usestrict';
constbluebird=require('bluebird');
constredis=require('redis');
bluebird.promisifyAll(redis.RedisClient.prototype);
module.exports=redis.createClient(process.env.REDIS_URL);
Nowwecancreateanewuserservicewithmethodsforgettingandsettingausername,insrc/services/users.js:
'usestrict';
letredisClient=require('../config/redis.js');
module.exports={
getUsername:userId=>
redisClient.getAsync(`user:${userId}:name`),
setUsername:(userId,name)=>
redisClient.setAsync(`user:${userId}:name`,name)
};
NotethattheRedisclientprovidesfunctionsforeachRediscommand(suchasgetandset).Bluebirdprovidespromise-basedversionsofeachfunctionsuffixedwithAsync.
Ofcourse,nowthatwehavetestinfrastructureforourproject,weshouldaddtestsfornewcodeaswegoasshownheretest/services/users.js:
'usestrict';
constexpect=require('chai').expect;
constservice=require('../../src/services/users.js');
describe('Userservice',function(){
describe('getUsername',function(){
it('shouldreturnapreviouslysetusername',done=>{
constuserId='user-id-1';
constname='UserName';
service.setUsername(userId,name)
.then(()=>service.getUsername(userId))
.then(actual=>expect(actual).to.equal(name))
.then(()=>done(),done);
});
it('shouldreturnnullifnousernameisset',done=>{
constuserId='user-id-2';
service.getUsername(userId)
.then(name=>expect(name).to.be.null)
.then(()=>done(),done);
});
});
});
Testingwithredis-js
Aswiththetestsforourgamesservice,wewanttobeabletointegratewithaRedisinstanceonourCIserver.Butwedon'twanttointroduceanynewdependenciesfordevelopment.Thistime,wewillmakeuseofalibrarycalledredis-jsforlocaltesting.UnlikeMockgoose,thisdoesnotuseanin-memoryversionoftherealDBengine(Redisisalreadyin-memory).Thisisinsteadare-implementationoftheNode.jsRedisclientthatstoresallofitsdatain-process:
>npminstallredis-js--save-dev
Nowwecancreateamoduleforobtainingtheenvironment-appropriateRedisreferenceasshownheresrc/config/redis.js:
'usestrict';
constbluebird=require('bluebird');
constdebug=require('debug')('hangman:config:redis');
if(process.env.REDIS_URL){
letredis=require('redis');
bluebird.promisifyAll(redis.RedisClient.prototype);
module.exports=redis.createClient(process.env.REDIS_URL);
}else{
debug('RedisURLnotfound.FallingbacktomockDB...');
letredisClient=require('redis-js');
bluebird.promisifyAll(redisClient);
module.exports=redisClient;
}
Notethat,unlikeMongoose,theNode.jsRedisclientcanbeusedimmediately.Anycommandsissuedbeforeithasconnectedareactuallyqueuedupinternally.Thismeanswecanjustreturntheclientfromthemoduleandrequireitdirectly.Therewouldn'tbeanybenefitinthiscasetothedependencyinjectionweusedwithMongoose.
WealsoneedtoaddRedistoour.travis.ymlfilesoitrunsontheCIserver:
services:
-mongodb
-redis-server
env:
global:
-MONGODB_URL=mongodb://localhost/hangman
-REDIS_URL=redis://127.0.0.1:6379/
Finally,weneedtoclosetheclientonceourtestshavecompleted,aswedidwithMongoose.Wealsoensureweemptythedatabaseonstartup(aswedon'thaveawayofdeletinguserdataviatheserviceinterface,aswedowithgames).Thefollowingcodeisfromtest/global.js:
'usestrict';
before(function(done){
require('../src/config/redis.js').flushdbAsync().then(()=>done());
});
after(function(done){
require('../src/config/redis.js').quit();
require('../src/config/mongoose.js').then(
(mongoose)=>mongoose.disconnect(done));
});
Thefollowingcodeisfromgulpfile.js:
letserver,teardown=(error)=>{
require('./src/config/redis.js').quit();
server.close(()=>
mongoose.disconnect(()=>done(error)));
};
ImplementinguserrankingswithRedisNowwearereadytoaddtheuserrankingfunctionalitytoourservice.Thefollowingcodeisfromsrc/services/users.js:
module.exports={
...
recordWin:userId=>
redisClient.zincrbyAsync('user:wins',1,userId),
getTopPlayers:()=>
redisClient.zrevrangeAsync('user:wins',0,2,'withscores')
.then(interleaved=>{
if(interleaved.length===0){
return[];
}
letuserIds=interleaved
.filter((user,index)=>index%2===0)
.map((userId)=>`user:${userId}:name`);
returnredisClient.mgetAsync(userIds)
.then(names=>names.map((username,index)=>({
name:username,
userId:interleaved[index*2],
wins:parseInt(interleaved[index*2+1],10)
})));
}),
getRanking:userId=>{
returnPromise.all([
redisClient.zrevrankAsync('user:wins',userId),
redisClient.zscoreAsync('user:wins',userId)
]).then(out=>{
if(out[0]===null){
returnnull;
}
return{rank:out[0]+1,wins:parseInt(out[1],10)};
});
}
};
MostoftheRediscommandsusedherewillbefamiliarfromearlierinthechapter.ThemostinterestingfunctionisgetTopPlayers.Thismakesuseofzrevrangewiththewithscoresoption.ThisreturnsanarrayofuserIDsandscores(interleavedtogether).Wemakeasecondrequesttothedatabaseusingmget(multivaluedget)toretrievethenamesofalltheusers.Oncethisreturnswecancombineallthedataforeachusertogetherintoanobject.
MakinguseoftheusersserviceWiringthisfunctionalityuptotherestofourapplicationdoesn'tuseanytechniqueswehaven'tseenbefore,soisomittedfromtheprintedcodelistingsforbrevity.Thefullimplementationcanbefoundinthecompanioncodeforthischapter,alongwithtestsfortherestoftheuserservicemethods,athttps://github.com/NodeJsForDevelopers/chapter09.
AnoteonsecurityWehavebeenrunningMongoDBandRediswiththeirdefaultout-of-the-boxsettings.Thisisfinefordevelopmentpurposes.Deployingtheseservicesintoproductionrequiresadditionalconsiderationaroundsecurity.Youcanfindmoreresourcesonthisathttps://docs.mongodb.com/manual/administration/security-checklist/andhttp://redis.io/topics/security.
SummaryInthischapter,wehaveunderstoodthedifferencebetweendifferenttypesofdatabaseandlearnedaboutthekeyfeaturesofMongoDBandRedis.Wealsopersistedourapplication'sdatausingthesedatabasesanduseddependencyinjectiontomakeourapplicationmoreflexible.Wealsolearnedhowtoconfigureourdevelopmentandintegrationenvironmentstouseappropriatedatabaseinstances.
Persistencemaybeconsideredthebottomlayerofoursystem.Inthenextchapter,we'llintroducereal-timeclient/servercommunicationintoourapplication.Thisfrontendfunctionalitymeansfocusingmoreonthetoplayerofoursystem.However,we'llalsoseeRedisplayinganimportantroleinsupportingthisfunctionality.
Chapter10.CreatingReal-timeWebAppsThewebhasofferedanevermoredynamicandinteractiveuserexperience.Throughoutthe90s,mostofthewebconsistedofstaticpagesorserver-siderenderedpages.Framesandiframesmadeitpossibletoreloadpartsofthepageinalimitedway.WhenAjaxappearedinthemid-2000s,itallowedpagestobemuchmoreengaging.Client-sideJavaScriptcouldnowrequestdatafromtheserverondemandandupdatethepagedynamically.
Real-timewebapplicationsarethenextstepinthisevolution.Theseareapplicationswheretheserverpushesdatatoclientswithouttheclientsneedingtoinitiatearequest.Thisallowsausertobenotifiedofnewinformationorforuserstointeractwitheachotherinrealtime.
Inthischapter,wewillcoverthefollowingtopics:
Establishingatwo-waycommunicationchannelbetweentheclientandserverAddingreal-timeinteractivitytoourapplicationIntroducingabackendtoscaleourreal-timeapplicationacrossmultipleservers
Understandingoptionsforreal-timecommunicationReal-timewebapplicationsneedabidirectionalcommunicationchannelbetweentheclientandtheserver.Thisisanypersistentconnectionthatallowstheservertopushadditionaldatatotheclientwhenneeded.TheWebSocketsprotocolisthemodernstandardforthiskindofcommunicationandisimplementedbymostbrowsers.
WebSocketconnectionsareinitiatedviaHTTP,butotherwisedonotdependonit.TheWebSocketprotocoldefinesawayofsendingmessagesbi-directionallyoveraTCPconnection.TCPisthelow-leveltransportprotocolthatusuallyunderliesHTTP.WebSocketsarestillarelativelynewtechnologyandnotfullysupportedbyallclientsandservers.MostmodernwebbrowserstodaydosupportWebSockets.However,intermediateservers(proxies,firewalls,andload-balancers)canpreventWebSocketconnectionsfromworking(eitherthroughlackofsupportorintentionallyblockingnon-HTTPtraffic).Inthesecases,therearealternativewaysofachievingreal-timecommunication.
TheEventSourcestandarddefinesawayforaservertosendeventstoclientsoverHTTPanddefinesaJavaScriptAPIforhandlingtheseevents.Itisnotasefficientorwidely-supportedasWebSockets,butisbettersupportedbysomeolderserversandclients.
Theultimatefallbackislong-polling.Thisiswhentheclientinitiatesanordinary(Ajax)requesttotheserver,whichstaysopenuntiltheserverhassomedatatosend.Assoonastheclientreceivesanydata,itmakesanotherrequesttotheserverforthenextmessage.ThisintroducesadditionalbandwidthoverheadsandlatencycomparedtoWebSockets,buthasthewidestsupportasitjustusesordinaryHTTPrequests.
Ideally,aclientandservercannegotiatetoworkoutthebestavailabletypeofconnectiontouse.Thiscanbequiteacomplicatedprocess,though.Fortunately,therearelibrarieswhichcanhandlethisforus.
IntroducingSocket.IOSocket.IOisamatureandwell-establishedlibrarywithexcellentcross-browsersupport.Itaimstoquicklyandreliablyestablishabidirectionalcommunicationchannelinacross-browsercompatibleway.Itprovidesaconsistentabstraction,basedonidiomaticJavaScriptevents,forreal-timecommunicationbetweentheclientandtheserveroverthischannel.IfyouhaveeverusedSignalRin.NET,youcanthinkofSocket.IOastheJavaScriptequivalent.
ImplementingachatroomwithSocket.IOLet'simplementachatlobbyforusersofourapplicationtotalktooneanother.First,weneedtoinstallSocket.IO:
>npminstall--savesocket.io
Theserver-sideimplementationforthisisverysimple.WejustneedtotellSocket.IOthat,wheneverausersendsachatmessage,wewanttobroadcastthistoallconnectedusersasgivenheresrc/realtime/chat.js:
'usestrict';
module.exports=io=>{
io.on('connection',(socket)=>{
socket.on('chatMessage',(message)=>{
io.emit('chatMessage',message);
});
});
};
Here,weaddalistenertoSocket.IO'sconnectionevent.Ourlistenerisfiredwheneveranewclientconnectstotheapplication.Thesocketvariablerepresentstheconnectiontothatspecificclient.
TheioparametershownpreviouslywillbeaSocket.IOinstance.Tocreateoneofthese,weneedtoprovideareferencetotheHTTPserverthatwillhostourapplication,sothatSocket.IOcanadditsownconnectionhandling.Tokeepthingstidier,we'lladdanewservermoduleinsrc/server.jstosetupourserver,startourExpressapplication,andinitializeSocket.IO:
'usestrict';
module.exports=require('./config/mongoose').then(mongoose=>{
constapp=require('../src/app')(mongoose);
constserver=require('http').createServer(app);
constio=require('socket.io')(server);
require('./realtime/chat')(io);
server.on('close',()=>{
require('../src/config/redis.js').quit();
mongoose.disconnect();
});
returnserver;
});
Thisalsoallowsustosimplifythebootstrapscriptandourintegrationtestsasinbin/www:
#!/usr/bin/envnode
vardebug=require('debug')('hangman:server');
varport=normalizePort(process.env.PORT||'3000');
require('../src/server').then((server)=>{
server.listen(port);
server.on('error',onError);
server.on('listening',onListening.bind(server));
}).catch(function(error){
debug(error);
process.exit(1);
});
...
functiononListening(){
varaddr=this.address();
...
}
...andingulpfile.js:
gulp.task('integration-test',
['lint-integration-test','test'],done=>{
constTEST_PORT=5000;
require('./src/server.js').then((server)=>{
server.listen(TEST_PORT);
server.on('listening',()=>{
gulp.src('integration-test/**/*.js')
.pipe(
...
}))
.on('error',error=>server.close(()=>done(error)))
.on('end',()=>server.close(done))
});
});
});
Nowweneedtoaddtheclient-sidecodetocommunicatewiththisservice.First,we'lladdaplaceforourchatlobbytotheapplicationhomepageasgivenheresrc/views/index.hjs:
{{/topPlayers}}
</ol>
<hr/>
<h3>Lobby</h3>
<formclass="chat">
<divid="messages"></div>
<inputid="message"/><inputtype="submit"value="Send"/>
</form>
</body>
</html>
Now,we'llcreatetheclient-sidescripttoconnectthiswiththeserverasgivenheresrc/public/scripts/chat.js:
$(document).ready(function(){
'usestrict';
varsocket=io();
$('form.chat').submit(function(event){
socket.emit('chatMessage',$('#message').val());
$('#message').val('');
event.preventDefault();
});
socket.on('chatMessage',function(message){
$('#messages').append($('<p>').text(message));
});
});
Finally,weneedtoincludeournewscriptinthepageandincludetheSocket.IOclient-sidescriptthatdefinestheprecedingiofunctionsrc/view/index.hjs:
<!DOCTYPEhtml>
<html>
<head>
<title>{{title}}</title>
<linkrel="stylesheet"href="/stylesheets/style.css"/>
...
<scriptsrc="/scripts/index.js"></script>
<scriptsrc="/socket.io/socket.io.js"></script>
<scriptsrc="/scripts/chat.js"></script>
</head>
<body>
...
Notethatwehaven'tcreatedthesocket.io.jsscriptanywhere.ThisisservedasaresultofattachingSocket.IOtoourserverinsrc/server.js.Sincewedon'tdefinetheiovariableinourownscript,weneedtoletESLintknowthatitexistsasaglobalvariableasgiveningulpfile.js:
gulp.task('lint-client',function(){
returngulp.src('src/public/**/*.js')
.pipe(eslint({envs:['browser','jquery'],
globals:{io:false}}))
.pipe(eslint.format())
.pipe(eslint.failAfterError());
});
Now,ifweopenupourapplicationintwobrowserwindows,theycansendchatmessagestoeachother!
Scalingreal-timeNode.jsapplicationsSinceourchatmessagesarebeingrelayedviatheserver,clientscancurrentlyonlycommunicatewithotherclientsconnectedtothesameserver.Thisisaproblemifwewanttoscaleourapplicationhorizontallyacrossmanyservers.
Thisiseasytofix,buttrickytodemonstrate.Todoso,weneedtohavetwoseparateinstancesofourapplicationrunning.Thiswillbemorerealisticandmoreusefuliftheyarealsousingthesameshareddatabasesforpersistence.SoweneedtostartupMongoDBandRedis,thenstarttwoinstancesofourapplicationondifferentports(sothattheydon'tcollide).
Thismeansrunningallofthefollowingcommands(replacingthedbpathofMongoDBasappropriateforyoursetup):
>redis-server
>mongod--dbpathC:\data\mongodb
>setMONGODB_URL=mongodb://localhost/hangman
>setREDIS_URL=redis://127.0.0.1:6379/
>setPORT=3000
>npmstart
>setPORT=3001
>npmstart
Thecommandsthatstartthedatabaseorapplicationserversalsooccupythecurrentconsole.So,tobeabletorunallofthesecommands,weneedtoexecutetheminseparatewindowsortellthemtoexecuteinthebackground.OnWindows,thiscanbeachievedwiththefollowingbatchscript:
@echooff
START/Bredis-server
START/Bmongod--dbpathC:\data\mongodb
setMONGODB_URL=mongodb://localhost/hangman
setREDIS_URL=redis://127.0.0.1:6379/
SLEEP2
setPORT=3000
START/Bnpmstart
SLEEP1
setPORT=3001
START/Bnpmstart
Nowyoucanconnectseparatebrowserstoaseparateapplicationinstanceathttp://localhost:3000andhttp://localhost:3001.Noticethattwoclientsconnectedtothesameapplicationinstancecanreceivemessagesfromeachother,butnotfromclientsontheotherapplicationinstance.
Toresolvethis,weneedasharedbackendthroughwhichalltheapplicationscancommunicate.Redisisaperfectcandidateforthis.
UsingRedisasabackendSocket.IOmakesuseoftheadapterpatterntosupportdifferentbackends.Anadapterisjustawrapperforconvertingoneinterfaceintoanother.Socket.IOhasastandardbackendinterfaceandvariousadapterstoallowdifferentimplementationstoworkwiththisinterface.Bydefault,itusesanin-memoryadapterthatislimitedtoasingleprocess.However,theSocket.IOprojectalsoprovidesanadaptorforusingRedisasabackend:
>npminstallsocket.io-redis--save
Onceinstalled,usingthisissimplyamatteroftellingSocket.IOwheretofindourRedisinstance(weskipthisintestenvironmentswhereweonlyhaveoneapplicationprocess)asgivenheresrc/server.js:
'usestrict';
module.exports=require('./config/mongoose').then(mongoose=>{
constapp=require('../src/app')(mongoose);
constserver=require('http').createServer(app);
constio=require('socket.io')(server);
if(process.env.REDIS_URL&&process.env.NODE_ENV!=='test'){
constredisAdapter=require('socket.io-redis');
io.adapter(redisAdapter(process.env.REDIS_URL));
}
require('./realtime/chat')(io);
...
returnserver;
});
Andthat'sit!Wedon'trequireanyotherchangestoourcodetosupportscalability.Ifyourestartyourapplicationinstancesnow,youshouldfindthatclientscancommunicatebetweenthem.
IntegratingSocket.IOwithExpressSofar,apartfromsharingthesameserver,theSocket.IOandExpresspartsofourapplicationarecompletelyindependent.Whileit'sgoodthattheyarelooselycoupled,somecross-cuttingconcernsmayberelevanttoboth.
Forexample,bothpartsofourapplicationshouldhaveamutuallyconsistentwayofidentifyingthecurrentuser.Thisisespeciallyimportantiftheyaretocometogethertoprovideasinglecoherentuserexperience.
First,let'sextendourusermiddlewaretoprovidethecurrentuser'snameaswellastheirID,bylookingthemupintheuserserviceasgivenheresrc/middleware/users.js:
'usestrict';
module.exports=(service)=>{
constuuid=require('uuid');
returnfunction(req,res,next){
letuserId=req.cookies.userId;
if(!userId){
userId=uuid.v4();
res.cookie('userId',userId);
req.user={
id:userId
};
next();
}else{
service.getUsername(userId).then(username=>{
req.user={
id:userId,
name:username
};
next();
});
}
};
};
Tip
Youcanfindupdatedtestsforthismiddlewareinthebook'scompanioncode.
Thiswillmeaninjectingouruserserviceasadependency,likewedofortheothermiddlewaremodules(thatis,routes)inourapplicationasgiveninsrc/app.js:
...
letgamesService=require('./service/games')(mongoose);
letusersService=require('./service/users');
letusers=require('./middleware/users')(usersService);
letroutes=require('./routes/index')(gamesService,usersService);
letgames=require('./routes/games')(gamesService,usersService);
letprofile=require('./routes/profile')(usersService);
...
TheinterestingpartisallowingSocket.IOtomakeuseofthismiddleware.Socket.IOhasitsownconceptofmiddlewareverysimilartothatofExpress.RecallthatExpressmiddlewarefunctionstakeparametersforthecurrentrequest,response,andanextcallback.Socket.IOmiddlewarefunctionsjusttakeacommunicationsocketandanextcallback.However,wecanaccesstheoriginalHTTPhandshakethatinitiatedthesocket.ThisallowsustoadaptourExpressmiddlewaretoSocket.IOmiddlewareanduseitasfollows,insrc/server.js:
'usestrict';
module.exports=require('./config/mongoose').then(mongoose=>{
letapp=require('../src/app')(mongoose);
letserver=require('http').createServer(app);
letio=require('socket.io')(server);
if(process.env.REDIS_URL){
letredisAdapter=require('socket.io-redis');
io.adapter(redisAdapter(process.env.REDIS_URL));
}
io.use(adapt(require('cookie-parser')()));
constusersService=require('./services/users.js');
io.use(adapt(require('./middleware/users')(usersService)));
require('./realtime/chat')(io);
...
returnserver;
});
functionadapt(expressMiddleware){
return(socket,next)=>{
expressMiddleware(socket.request,socket.request.res,next);
};
}
NowtheusermiddlewarewillrunforSocket.IOaswellasregularHTTPrequests,makinguserdataavailabletoSocket.IOaswell.Let'susethistoincludeusernamesinourchat.First,weneedtoupdateourserverasgiveninsrc/realtime/chat.js:
'usestrict';
module.exports=io=>{
io.on('connection',(socket)=>{
socket.on('chatMessage',(message)=>{
io.emit('chatMessage',{
username:socket.request.user.name,
message:message
});
});
});
}
NoticethatSocket.IOallowsustosendobjectsinsteadofsimplestringsastheeventpayload.Nowwejustneedtomakeuseofthisintheclientasgivenheresrc/public/scripts/chat.js:
$(document).ready(function(){
'usestrict';
varsocket=io();
...
socket.on('chatMessage',function(data){
$('#messages').append(
$('<p>').text(data.message)
.prepend($('<b>').text(data.username)));
});
Ifyounowopentheapplicationinseparatebrowsersessionsandspecifydifferentusernames,youwillseetheseinthechatoutput.
DirectingSocket.IOmessagesNowthatwehaveaccesstousernames,wecanalsoannouncethearrivalofusersinthelobby.WecandothisbyextendingourSocket.IOconnectioneventhandlerasgivenheresrc/realtime/chat.js:
'usestrict';
module.exports=io=>{
io.on('connection',(socket)=>{
constusername=socket.request.user.name;
if(username){
socket.broadcast.emit('chatMessage',{
username:username,
message:'hasarrived',
type:'action'
});
}
socket.on('chatMessage',(message)=>{
io.emit('chatMessage',{
username:username,
message:message
});
});
});
}
Here,weusesocket.broadcast.emit,ratherthanio.emit,tosendtheeventtoallclientsexceptforthecurrentsocket.Notethatwealsoaddextradatatothemessage.Thistimeweaddatypefield(setto'action'forthearrivalmessage)toallowdifferentvisualpresentationofdifferenttypesofmessage.Wecanachievethisbyupdatingourclient-sidecodetosetadditionalCSSclassesbasedonthemessagetypeasgivenheresrc/public/scripts/chat.js:
socket.on('chatMessage',function(data){
$('#messages').append(
$('<p>').text(data.message).addClass(data.type)
.prepend($('<b>').text(data.username)));
});
Tip
YoucanfindtheCSSfilefortheexampleapplicationinthecompanioncode.
Let'salsoenforcethatusershavetochooseausernamebeforetheycantakepartinthechatasgivenheresrc/realtime/chat.js:
'usestrict';
module.exports=io=>{
io.on('connection',(socket)=>{
...
socket.on('chatMessage',(message)=>{
if(!username){
socket.emit('chatMessage',{
message:'Pleasechooseausername',
type:'warning'
});
}else{
io.emit('chatMessage',{
username:username,
message:message
});
}
});
});
}
Here,weusesocket.emitratherthanio.emittosendamessagetotheclientassociatedwiththecurrentsocket.
TestingSocket.IOapplicationsNowlet'slookathowwecantestourchatmodule.Totalktoitfromourtestswe'llneedaSocket.IOclient.TheSocket.IOprojectprovidesanotherpackageforthis:
>npminstallsocket.io-client--save-dev
Theinfrastructureforourtestsconsistsofsettingupaserverandmultipleclientsasgivenheretest/realtime/chat.js:
'usestrict';
describe('chat',function(){
constexpect=require('chai').expect;
letserver,io,url,createUser,createdClients=[];
beforeEach(done=>{
server=require('http').createServer();
server.listen((err)=>{
if(err){
done(err);
}else{
constaddr=server.address();
url='http://localhost:'+addr.port+'/chat';
io=require('socket.io')(server);
require('../../src/realtime/chat.js')(io);
done();
}
});
});
afterEach(done=>{
createdClients.forEach(client=>client.disconnect());
server.close(done);
});
constcreateClient=require('socket.io-client');
createUser=(name,room)=>{
letuser={
name:name,
client:createClient(url)
};
createdClients.push(user.client);
returnuser;
};
});
Here,wecreateanHTTPserverwithoutspecifyinganaddress,sothattheOSwillassignusanavailableport.Wethenusethisthisservertohostourchatimplementation.
Sincewe'rerunningthechatmoduleinisolation,wedon'thaveourusersmiddlewareavailable,
sowillneedanalternativewaytoprovideusernames.Wecandothiswithastubmiddlewareinourteststhatreadsusernamesdirectlyfromaheader:
'usestrict';
describe('chat',function(){
constexpect=require('chai').expect;
letserver,io,url,createUser,createdClients=[];
beforeEach(done=>{
server=require('http').createServer();
server.listen((err)=>{
if(err){
done(err);
}else{
constaddr=server.address();
url='http://localhost:'+addr.port;
io=require('socket.io')(server);
io.use((socket,next)=>{
socket.request.user={
name:socket.request.headers.username
};
next();
});
require('../../src/realtime/chat.js')(io);
done();
}
});
});
...
constcreateClient=require('socket.io-client');
createUser=(name,room)=>{
letheaders={};
if(name){
headers.username=name;
}
letuser={
name:name,
client:createClient(url,{extraHeaders:headers})
};
createdClients.push(user.client);
user.client.emit('joinRoom',room);
returnuser;
};
});
Nowwearereadytoimplementourtests.Thefirsttwo,formessagesinitiatedfromtheserver,
arequitesimple:
it('warnsunnameduserstochooseausername',done=>{
letunnamedUser=createUser();
unnamedUser.client.emit('chatMessage','Hello!');
unnamedUser.client.on('chatMessage',(data)=>{
expect(data.message).to.contain('chooseausername');
expect(data.username).to.be.undefined;
expect(data.type).to.equal('warning');
done();
});
});
it('broadcastsarrivalofnamedusers',done=>{
letconnectedUser=createUser();
letnewUser=createUser('User1');
connectedUser.client.on('chatMessage',(data)=>{
expect(data.message).to.contain('arrived');
expect(data.username).to.equal(newUser.name);
expect(data.type).to.equal('action');
done();
});
});
Testingmessagessentbetweenclientsrequiresalittlemorecaretocaptureeachclient'sreceiptofthemessage:
it('emitsmessagesfromnamedusersbacktoallusers',done=>{
letnamedUser=createUser('User1');
letotherUser=createUser();
letmessageReceived=function(data){
this.received=data;
if(namedUser.received&&otherUser.received){
[namedUser.received,otherUser.received]
.forEach(received=>{
expect(received.message).to.equal('Hello!');
expect(received.username)
.to.equal(namedUser.name);
});
done();
}
};
otherUser.client.on('chatMessage',
messageReceived.bind(otherUser));
namedUser.client.on('chatMessage',
messageReceived.bind(namedUser));
namedUser.client.emit('chatMessage','Hello!');
});
OrganizingSocket.IOapplicationsNowthatwehaveachatlobbyontheindexpageofourapplication,it'sabitoddthatusershavetoreloadthepage(andlosethechathistory)tofindoutaboutnewgames.WecanuseSocket.IOtoupdatetheseaswell.
Exposingreal-timeupdatestothemodelFirst,we'llneedourgamesserviceitselftoexposeeventsforwhengamesareaddedorremoved.HereweusetheMongoose-providedpostmethodtohookintopersistenceoperationsongamesasgivenheresrc/services/games.js:
'usestrict';
constEventEmitter=require('events');
constemitter=newEventEmitter();
module.exports=(mongoose)=>{
letGame=mongoose.models['Game'];
if(!Game){
letSchema=mongoose.Schema;
letgameSchema=newSchema({
word:String,
setBy:String
});
...
gameSchema.post('save',game=>
emitter.emit('gameSaved',game));
gameSchema.post('remove',game=>
emitter.emit('gameRemoved',game));
Game=mongoose.model('Game',gameSchema);
}
return{
...
get:id=>Game.findById(id),
events:emitter
};
};
module.exports.events=emitter;
Weexposeaneventemittertoallowothermodulestosubscribetoeventsforwhengamesareaddedorremoved.Eventemittersareabuilt-infeatureofNode.js,whichprovideasimplewaytoexposecustomevents.NotethattheMongooseSchemaclassisitselfaneventemitter,sowecouldjustexposethisdirectly.However,thiswouldbeleakingdetailsabouttheimplementationofourgamesservice.
Tip
Again,youcanfindnewtestsforthesechangesinthecompanioncode.
OrganizingSocket.IOapplicationsusingnamespacesReal-timechatandreal-timeupdatestothelistofgamesarequitedistinctfunctionalareasofourapplication.Socket.IOprovidesnamespacestoallowustoorganiseevents.Thisallowsustostilluseasingleconnectionbetweentheclientandtheserver,withouthavingtoworryaboutclashingeventnamesbetweendifferentfunctionalareas.Thisisveryusefulasapplicationsbecomelargerandmorecomplex.
Puttingourchatfunctionalityunderanamespaceisaverysimplechangeontheclientandtheserver(andinourtests).
Thefollowingcodeisfromsrc/public/scripts/chat.js:
$(document).ready(function(){
'usestrict';
varsocket=io('/chat');
...
Thefollowingcodeisfromsrc/realtime/chat.js:
'usestrict';
module.exports=io=>{
constnamespace=io.of('/chat');
namespace.on('connection',(socket)=>{
...
socket.on('chatMessage',(message)=>{
if(!username){
...
}else{
namespace.emit('chatMessage',{
username:username,
message:message
});
}
});
});
};
Thefollowingcodeisfromtest/realtime/chat.js:
constaddr=server.address();
url='http://localhost:'+addr.port+'/chat';
NowwecanaddanewSocket.IOmoduleforexposingchangestogames.ThissimplyneedstoforwardeventsfromourgamesservicetoconnectedSocket.IOclients.
Weaddthefollowingcodeundersrc/realtime/games.js:
'usestrict';
module.exports=(io,service)=>{
io.of('/games').on('connection',(socket)=>{
forwardEvent('gameSaved',socket);
forwardEvent('gameRemoved',socket);
});
functionforwardEvent(name,socket){
service.events.on(name,game=>{
if(game.setBy!==socket.request.user.id){
socket.emit(name,game.id);
}
});
}
};
Wealsoneedtoincludethismoduleintheinitialisationofourserver.
Thefollowingcodeisfromsrc/server.js:
'usestrict';
module.exports=require('./config/mongoose').then(mongoose=>{
...
require('./realtime/chat')(io);
constgamesService=require('./services/games.js')(mongoose);
require('./realtime/games')(io,gamesService);
...
returnserver;
});
Thecorrespondingclientjustneedstoconnecttothe/gamesnamespaceandupdatethelistaccordingly.
Thefollowingcodeisfromsrc/public/scripts/index.js:
varsocket=io('/games');
varavailableGames=$('#availableGames');
socket.on('gameSaved',function(game){
availableGames.append(
'<liid="'+game+'"><ahref="/games/'+game+'">'+
game+'</a></li>');
});
socket.on('gameRemoved',function(game){
$('#'+game).remove();
});
Thefollowingcodeisaddedtosrc/views/index.hjs:
<h3>Gamesavailabletoplay</h3>
<ulid="availableGames">
{{#availableGames}}
<liid="{{id}}"><ahref="/games/{{id}}">{{id}}</a></li>
{{/availableGames}}
</ul>
Tip
Inpractice,itwouldbettertouseaclient-sideMV*librarysuchasKnockoutorBackbonetoupdatethepagebasedonmodelchanges,ratherthanmanipulatingtheDOMlikethis,butthat'soutsidethescopeofthisbook.
Now,ifyouopentheapplicationintwoseparatebrowsersessionsandcreateanewgameinonebrowserwindow,itwillimmediatelyappearintheother.
PartitioningSocket.IOclientsusingroomsThefinalpieceoffunctionalitywe'regoingtoaddinthischapteristheabilityforusersplayingthesamegametotalktooneanother.Wecanre-usethechatfunctionalitywe'vealreadywrittenforthis.However,wewantaseparatechatforthelobbyonthehomepageandforeachgame.
Socket.IOprovidesroomsfordirectingmessagestodifferentgroupsofclients.Rememberthatnamespacesallowustodivideourapplicationintodifferentfunctionalareas.Roomsallowustodivideupclientswithinthesamefunctionalarea.
RoomsinSocket.IOarejuststringidentifiersandweaddclientstoaroomusingthesocket.joinfunction.We'llintroduceanewjoinRoomeventtoallowourclientstoaskourservertoaddthemtoaparticularroom.We'llrespondtothiseventontheserverasfollows:
Thefollowingcodeisfromsrc/realtime/chat.js:
'usestrict';
module.exports=io=>{
constnamespace=io.of('/chat');
namespace.on('connection',(socket)=>{
constusername=socket.request.user.name;
socket.on('joinRoom',(room)=>{
socket.join(room);
if(username){
socket.broadcast.to(room).emit('chatMessage',{
username:username,
message:'hasarrived',
type:'action'
});
}
socket.on('chatMessage',(message)=>{
if(!username){
...
}else{
namespace.to(room).emit('chatMessage',{
username:username,
message:message
});
}
});
socket.on('disconnect',()=>{
if(username){
socket.broadcast.to(room).emit('chatMessage',{
username:username,
message:'hasleft',
type:'action'
});
}
});
});
});
};
Notethatwealsoannouncewhenusersleaveaparticularroom,inthesamewaythatweannouncearrivals.Again,youcanfindtheadditionaltestforthisfunctionalityintheexamplecode.
We'lladdthechatfunctionalityintothegamepageandspecifythecorrectroomusingadataattributeonthechatform.
Thefollowingcodeisfromsrc/views/game.hjs:
<!DOCTYPEhtml>
<html>
<head>
<title>Hangman-Game#{{id}}</title>
<linkrel="stylesheet"href="/stylesheets/style.css"/>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js">
</script>
<scriptsrc="/scripts/game.js"></script>
<scriptsrc="/socket.io/socket.io.js"></script>
<scriptsrc="/scripts/chat.js"></script>
<basehref="/games/{{id}}/">
</head>
<body>
<h1>Hangman-Game#{{id}}</h1>
<h2id="word"data-length="{{length}}"></h2>
<p>Pressletterkeystoguess</p>
<h3>Missedletters:</h3>
<pid="missedLetters"></p>
<hr/>
<h3>Discussion</h3>
<formclass="chat"data-room="{{id}}">
<divid="messages"></div>
<inputid="message"/><inputtype="submit"value="Send"/>
</form>
</body>
</html>
Thefollowingcodeisfromsrc/views/index.hjs:
<hr/>
<h3>Lobby</h3>
<formclass="chat"data-room="lobby">
<divid="messages"></div>
<inputid="message"/><inputtype="submit"value="Send"/>
</form>
Thenweneedtoupdatetheclientscripttojointhecorrectroomwhenconnecting.
Thefollowingcodeisfromsrc/public/scripts/chat.js:
$(document).ready(function(){
'usestrict';
varchat=$('form.chat');
varsocket=io('/chat');
socket.emit('joinRoom',chat.data('room'));
chat.submit(function(event){
...
});
...
});
Finally,weneedtomakesurethattypingachatmessagedoesn'tinterferewithplayingthegame.Wecandothisbyonlytreatingkeypressesasguessesforthegamewhentheuserisn'ttypinginthechatmessagebox.
Thefollowingcodeisfromsrc/public/javascript/game.js:
$(document).keydown(function(event){
if(!$('.chat#message').is(':focus')&&
event.which>=65&&event.which<=90){
varletter=String.fromCharCode(event.which);
if(guessedLetters.indexOf(letter)===-1){
guessedLetters.push(letter);
guessLetter(letter);
}
}
});
Tip
Youcanfindnewandupdatedtestsforthisfunctionalityinthecompanioncode.
Puttingthisalltogether,wecannowhavemultipleclientstalkingtooneanotherinseparaterooms:
SummaryInthischapter,wehavecreatedareal-timeclient/servercommunicationchannelusingSocket.IO,usedRedisasabackendtoscaleareal-timeapplicationhorizontally,integratedSocket.IOwithExpressmiddleware,andorganizedourapplicationusingSocket.IOnamespacesandrooms.
Asthenetworkconnectivityofourapplicationisbecomingmorecomplicated,it'smoreimportanttotesttheapplicationonawebserveroutsideofthedevelopmentorCIenvironment.Inthenextchapter,we'lllookathowtodeployourapplicationtotheweb.
Chapter11.DeployingNode.jsApplicationsSofar,wehaveonlyrunourapplicationinourlocaldevelopmentenvironment.Inthischapter,wewilldeployittotheWeb.Therearemanydifferentoptionsforhostinganapplication.Wewillworkthroughonedeploymentoptiontoquicklygetanapplicationupandrunning.WewillalsodiscussbroaderprinciplesandalternativeoptionsfordeployingNode.jsapplications.
Inthischapter,wewillcoverthefollowingtopics:
DeployingourapplicationtotheWebUsingapplicationlogstodiagnoseissuesonremoteserversSettingupdatabaseserversandenvironmentalconfigurationDeployingautomaticallyfromTravisCI
Tip
Ifyouwanttofollowalongwiththischapter,youcanusethecodefromhttps://github.com/NodeJsForDevelopers/chapter10/asastartingpoint.ThiscontainstheexamplecodefromtheendofChapter10,CreatingReal-timeWebApps,whichwewillbuildoninthischapter.
WorkingwithHerokuHerokuisacloud-basedplatformforwebapplications.Itaimstoallowdeveloperstofocusonapplicationsratherthaninfrastructure.Itprovidesalow-frictionworkflowfordeployinganewapplicationquickly,whilealsosupportinglong-termscalability.Italsooffersamarketplaceofadd-onservices,suchasdatabasesandmonitoring.
ThereareseveralsimilarservicestoHeroku,someofwhichwewillcoverlaterinthischapter.Herokuwasoneofthefirstservicesofitskind.Inparticular,itwasoneofthefirsttosupportNode.jsasafirst-classcitizen.Italsooffersmanyfeaturesforfree,includingeverythingneededfortheworkedexampleinthissection.
Note
NotethatHeroku'sfreefeaturesaresufficientfordeployinganapplicationfordevelopment,demonstration,orexperimentalpurposes.Itwouldnotbesufficientforaproductiondeploymentofanapplicationservingendusers.Seehttps://www.heroku.com/pricingfordetailsofHeroku'spricingtiers.
SettingupaHerokuaccountandtoolingTofollowtheexampleinthissection,youwillfirstneedtosignupforHerokuathttps://signup.heroku.com/.
Wewillalsobeusingtheherokutoolbelt,aCLIforconfiguringHeroku.Downloadandinstalltheversionforyourplatformfromhttps://toolbelt.heroku.com/.
Checkthattheherokutoolbeltisinstalledcorrectlyandavailableonyourpath.Openanewcommandpromptandrunthefollowingcommand:
>heroku
Youshouldseethehelptextwithalistofavailablecommands.ConfigurethetoolbelttouseyourHerokuaccountbyrunningthefollowingcommand:
>herokulogin
RunninganapplicationlocallywithHerokuHerokurequiresasmallconfigurationfile(similarto.travis.yml)tellingithowtorunourapplication.ThisisafilenamedProcfile,whichinourcasecontainsasinglelineasfollow:
web:npmstart
ThistellsHerokuthatourapplicationconsistsofasinglewebprocess,whichcanbestartedwithnpmstart.
Note
Note,especiallyifyouareusedtotheWindowsfilesystem,thattheuppercasePinthefilenameisimportant.TheapplicationwillbedeployedtoaUnix-likesystem,wherefilenamesarecase-sensitive.
ToverifyourProcfile,wecanrunourapplicationlocallyusingHeroku:
>herokulocal
ThiswilllaunchourapplicationusingtheProcfile.Notethatitalsosetsadefaultportof5000.Youshouldnowbeabletovisittheapplicationathttp://localhost:5000.
Theherokulocalcommandalsosetsupenvironmentvariablesforourapplication.Thesearereadfromalocal.envfileattherootofourapplication:
MONGODB_URL=mongodb://localhost/hangman
REDIS_URL=redis://127.0.0.1:6379/
YoucantestthisbystartinguplocalinstancesofMongoDBandRedis.Runthefollowingcommandsinseparateprompts(settingthe--dbpathasappropriate):
>redis-server
>mongod--dbpathC:\data\mongodb
>herokulocal
Havingthis.envfilemeansthatwecanusenpmstartdirectly(aswehavebefore)torunwithmockdatastoresandherokulocalwhenwewantamorerealisticenvironment,withouthavingtokeeptrackofourcurrentenvironmentvariables.
DeployinganapplicationtoHerokuNowthatwehavecreatedaProcfile,deployingourapplicationtothewebiseasy.First,weneedtocreateanewHerokuapplication:
>herokucreate
Bydefault,thisprovisionsaminimalapplicationonHeroku,witharandomlyassignedname.Youcanoptionallyspecifyanapplicationnameasathirdparameter.
ThiscommandalsoreturnsthepublicURLforournewly-createdapp,whichwecanvisitnow.Thefollowingresponseisreturned:
There'snotmuchtoseebecausewehaven'tdeployedanythingyet.ThequickestwaytodeployanapplicationtoHerokuisviaGit.TheherokucreatecommandalsocreatedanewGitremoteforustopushto.YoucanseethisbyviewingthelistofGitremotes:
>gitremote-v
WenowhaveaGitremotenamedheroku.MakesurethenewProcfilehasbeencommitted.Now,whenwepushourmasterbranchtothisremote,itisautomaticallybuiltanddeployed:
>gitpushherokumaster
Ifwevisittheapplication'sURLagainnow,weseethefollowing:
Ourapplicationhasdeployedbutisnowreturninganerror.Todiagnosetheproblemwithourapplication,we'llneedtolookatthelogs.
WorkingwithHerokulogs,config,andservicesWecanviewthelogsfromourapplicationbyrunningherokulogs.Ifyoulookthroughthelogstotheerrorstacktrace,you'llseethefollowingerrormessage:
app[web.1]:Error:Cannotfindmodule'mockgoose'
ThemockgoosepackageisunavailablebecauseHerokubuildsourapplicationusingthedependenciesinpackage.jsonandnotthedevDependencies.RecallfromChapter9,PersistingData,thatthiserrorisintentional.WewantedtheapplicationtofailinliveenvironmentsifnoMongoDBURLisconfigured.
Tofixthiserror,weneedtosetupaMongoDBinstanceandconfigureourapplicationtoconnecttoit.We'llneedtodothesameforourRedisDB.BothofthesedatastoresareavailableasservicesfromtheHerokumarketplace.
SettingupMongoDB
WecanaddHerokumarketplaceservicesviathecommandline.MongoLabisathird-partyserviceprovidingMongoDBinstances.Wecanaddaninstancetoourapplicationasfollows:
>herokuaddons:createmongolab:sandbox
Thiscreatesasandbox(freetier)MongoDBinstance,suitablefordemopurposes.NotefromtheoutputofthiscommandthatitalsocreatedaMONGOLAB_URIconfigvariable.Herokuwillprovidethistoourapplicationasanenvironmentvariableatruntime.
OurapplicationisexpectinganenvironmentvariablenamedMONGODB_URL.We'llneedtocreatethisandsetittothesamevalueasMONGOLAB_URI.Youcanviewandsetconfigvariablesforanapplicationasfollows:
>herokuconfig
>herokuconfig:setMONGODB_URL=mongodb://...
YoushouldfillinthevalueofMONGODB_URLtomatchthevalueofMONGOLAB_URIreturnedbythefirstcommand.
SettingupRedis
HerokualsoprovidesaRedisserviceviaitsmarketplace.We'lladdittoourapplicationasfollows:
>herokuaddons:createheroku-redis:hobby-dev--as:REDIS
Againweusethefreetierversionofthisservice(hobby-dev)fordemopurposes.It'seasytore-scaleservicestodifferenttierslater.
TheRedisservicealsoallowsyoutospecifyanaliasforthecreatedserviceinstance.Aliases
arespecifiedusingthe--asparameterwithherokuaddons:create.ThisisusefulforRedisaswemayhaveseveralRedisinstancesassociatedwithasingleapplication.It'sparticularlyusefulforus,since,byaliasingourinstanceasREDIS,HerokuwillcreateaREDIS_URLenvironmentvariable.Thisisexactlywhatourapplicationexpectstosee.
Theherokuaddons:createcommandrestartsourapplicationimmediately.Ournewdatabaseinstanceswilltakeaminuteortwotobecomeavailablethough.Waitaminutebeforerestartingtheapplication:
>herokurestart
WecannowvisittheapplicationURLinourbrowserandseeitrunningontheWeb!
DeployingfromTravisCIDeployingviaGitisaquickwaytogetupandrunningandisusefulfordevelopers.It'snotarobustwayofpushingoutchangesthough.IfwearepracticingContinuousDeliverythenwemaywanttodeployoneverycommit,atleasttoaUATenvironment.ButwestillwantourCIservertoactasagatekeeperandensurethatweonlydeploygoodbuilds.
TravisCIsupportsdeploymenttoawiderangeofhostingproviders(aswellasarbitrarydeploymentviacustomscripts).WecantellTravisCItodeploytoHerokubyaddingadeploysectiontoourtravis.ymlasfollows(replacingapplication-name-12345withthenameofourpreviouslycreatedHerokuapplication):
services:
-mongodb
-redis-server
deploy:
provider:heroku
app:application-name-12345
api_key:
env:
global:
-MONGODB_URL=mongodb://localhost/hangman
-REDIS_URL=redis://127.0.0.1:6379/
TravisCIwillonlydeployourapplicationifthebuildpasses.InorderforTravisCItocommunicatewithHeroku,itrequiresourHerokuAPIkey.Butwemaynotwanttocommitthistosourcecontrol(especiallyifourGitrepositoryispublic).TravisCIallowsyoutoavoidthisbyspecifyingencryptedenvironmentvariablesforthebuild.
SettingencryptedTravisCIenvironmentvariablesEnvironmentvariablescanbeencryptedusingapublickeythatTravisCIassociateswithourrepository.TravisCIthenusesthecorrespondingprivatekeytodecryptthesevariablesatbuildtime.
TheeasiestwaytoencryptenvironmentvariableswiththecorrectkeyistousetheTravisCLI.ThisisavailableasaRubypackage.
InstallingRuby
IfyoudonothaveRubyinstalledonyoursystemalready,seehttps://www.ruby-lang.org/en/documentation/installation/.ThebestwaytoinstallonWindowsistouseRubyInstaller,fromhttp://rubyinstaller.org/.
YoucancheckwhetherRubyisinstalledandconfiguredonyourpathbyrunningthefollowingcommand:
>ruby-ver
Youshouldhaveversion2.0.0orhigher.
Creatinganencryptedenvironmentvariable
OnceyouhaveRubyinstalledandonyourpath,youcaninstalltheTravisCLIasfollows:
>geminstalltravis--no-rdoc--no-ri
Note
GemistheRubypackagemanager,similartonpm.The--no-docand--no-riargumentshereskipinstallationoflow-levelAPIdocs,whichwedon'tneed.
Nowwecanaddourencryptedenvironmentvariable.FirstweneedtoobtaintheHerokuAPIkeyforourapplication:
>herokuauth:token
Nowwecanaddthistoour.travis.ymlfileasfollows:
>travisencrypt[AUTH_TOKEN]--adddeploy.api_key
[AUTH_TOKEN]istheoutputfromthepreviouscommand.
ThisencryptstheAPIkeyandautomaticallyaddstheencryptedversionintoour.travis.ymlfile.Beforecommitting,tryupdatingsomethingintheapplication,forexamplethepagetitlefromsrc/routes/index.js:
...
.then(results=>{
res.render('index',{
title:'Hangmanonline',
userId:req.user.id,
createdGames:results[0],
...
Nowcommitandpushthemasterbranch(toorigin,notdirectlytoheroku)andwaitfortheTravisCIbuildtocomplete.Thebuildoutputshowsourapplicationbeingdeployed:
Ifyouvisittheapplicationagain,youshouldseethenewversionwiththeupdatedtitle.
RecallthatTravisCIisactuallybuildingourapplicationformultipleversionsofNode.js.Bydefault,TravisCIdeploysourapplicationattheendofeachbuildjob.Thisisunnecessaryandslowsdownouroverallbuild.WecantellTravisCItodeployonlyfromaspecificbuildjobbyalteringour.travis.ymlfileasfollows:
deploy:
provider:heroku
app:afternoon-cliffs-85674
on:
node:6
api_key:
secure:...
IfwecommitandchecktheoutputfromTravisCIagain,wecanseethatonlytheNode.jsv6buildjobperformsadeployment.
FurtherresourcesForfurtherconsiderationsondeployingwebapps,seeTheTwelve-FactorApp(http://12factor.net/).Thisisadetailedresourceaboutimportantconsiderationsforrunningenterprise-gradewebapplicationsonservicessuchasHeroku.
Thereare,ofcourse,agreatmanyoptionsforhostingawebapplication.Azure'swebappserviceandAWS'sElasticBeanstalkbothsupportNode.jsasafirst-classcitizen.Modulus(https://modulus.io/)providesNode.jsandMongoDBhosting,withpowerfulscaling,monitoring,andload-balancingfeatures.
Theprecedingareallexamplesofapplicationhostingplatforms(Platform-as-a-Service(PaaS),incloudterminology).Youcan,ofcourse,alsodeployNode.jsapplicationstobareinfrastructure(eithercloudinfrastructureoryourownmachines).Foradetailedguide,seehttps://certsimple.com/blog/deploy-node-on-linux.
Youmayneedtomanagereleasesofyourapplicationthroughmultipleenvironments.YourCIservermightfirstdeployyourapplicationtoanintegrationenvironmentandruntestsonittherebeforedeployingtoUAT.YoumaythenwanttobeabletopushtheexactsamereleasefromUATtoStageandLiveenvironmentsattheclickofabutton.
HerokuPipelinesandAzureWebAppdeploymentslotsallowyoutomanagethereleaseofyourapplicationthroughdifferentenvironments.Wercker(http://wercker.com/)isabuildanddeploymentservicethatcanautomatemorecomplexworkflows.ItalsoprovidesisolatedenvironmentsbasedonDockercontainers.
SummaryInthischapter,wehavedeployedanapplicationtothewebusingHeroku,configuredenvironmentsettingsandprovisioneddatabases,setupTravisCItoautomaticallydeploysuccessfulbuilds,andlearnedaboutfurtheroptionsandconsiderationsfordeployingNode.jsapplications.
Nowthatourapplicationisavailableonline,wecanstartthinkingabouthowtointegrateitwiththewiderWeb.Inthenextchapter,we'lllookatallowinguserstologinusingthirdpartysocialmediaservicesasanidentityprovider.
Chapter12.AuthenticationinNode.jsTheapplicationwehavebuiltsofarallowsuserstochooseausernametoidentifythemselves.However,theyonlyretainthisidentityforthedurationoftheirbrowsersession.It'simportanttoallowuserstoretainaconsistentidentityfromonesessiontothenext.Thisallowsustobuildricheruserexperiences.Somewebsites(suchasFacebook)couldn'toffertheirmainfunctionalityatallwithoutbeingabletoidentifyusers.
Identifyingusersrequiresustoimplementauthentication.Inthischapter,wewillcoverthefollowingtopics:
Implementingthird-partyauthenticationviasocialnetworkingsitesAssociatingthird-partyidentitieswithourownuserdataSimulatinguserauthenticationtosupportintegrationtesting
IntroducingPassportPassportisanauthenticationframeworkforNode.js.ItcanactasExpressmiddleware,makingiteasytointegratewithourapplication.
Likesomeoftheotherlibrarieswe'vediscussedsofar,Passportisverymodular.Itscorepackageprovidesacommonparadigmforauthentication.Passport'smiddlewareperformsauthenticationandaugmentstherequestobjectwithauserproperty.
AdditionalPassportnpmpackagessupporthundredsofdifferentstrategiesforauthentication.EachPassportstrategyprovidesadifferentmechanismforidentifyingusers.We'lllookatafewofthesestrategiesinthischapter.Passportmakesiteasytoaddnewstrategiestosuittheneedsofeachapplication.
ChoosinganauthenticationstrategyAcommonintroductoryexampleisusername/password-basedauthentication.Thisusesaloginformtoverifyusers'credentialsagainsttheapplication'sdatabase.Althoughthisisoneofthesimplestauthenticationmechanismstounderstand,it'snotthemostuseful.Forcinguserstocreateanaccountforoursiteisanextrahurdletothemusingit.Usersalsogettiredofcreatinganaccountandpickingapasswordforeverynewwebsite.
Passportdoessupportthiskindofauthentication,viathepassport-localstrategy.We'llmakeuseofthisstrategyfortestpurposeslateroninthischapter,butnotinourproductioncode.It'sbettertoallowuserstoauthenticateusinganidentityalreadyestablishedelsewhere.Thissavesusersfromhavingtopicknewcredentialsandalsosavesourwebsitefromhavingtomanagethese.Thisisjustgoodseparationofconcerns.
IfyoulogintoStackOverflow,you'llnoticethatitsuggestslogginginusingGoogle+orFacebook.ItalsosupportsOpenIDandotherproviders.Implementingsupportforeachoftheseloginmechanismsfromscratchwouldbealotofwork.FortunatelytherearePassportstrategiesforallofthem.
Understandingthird-partyauthenticationPassportwilldomostoftheheavyliftingforus,butit'sstillworthhavingabasicunderstandingofhowthird-partyauthenticationworks.Whenaclientwantstologintoawebsite,itsendsthemtoathird-partyprovider.Thethird-partyprovidergivestheclientbackatokentheycanusetoauthenticatewiththewebsite.Whentheclientisawebbrowser,thisprocesscanbemadealmostinvisibletotheuser,viaautomaticredirects.
Thewebsitemustthenverifythatthetokenpresentedtoitbytheclientreallycamefromthethird-partyprovider.Thewebsiteandthethird-partyprovidermighthaveestablishedapre-sharedkeyforthispurpose,whichcouldbeusedtocreateacryptographicallyverifiabletoken.Alternatively,thewebsitemightcallthethird-partyproviderdirectlytoverifythetoken.Inpractice,awebsitewilloftenwanttocallathird-partyprovideranywaytogainmoreinformationassociatedwiththeuser'sidentity,forexample,theirusernameorotherprofileinformation.
UsingExpresssessionsManyofPassport'sstrategiesarebasedonHTTPsessions.Atthemoment,ourapplicationisjustusingsimplecookiestostoreuserIDs.TousePassportforthird-partyauthentication,we'llneedtoaddsessionsupportintoourapplication.Expressprovidessessionsupportintheexpress-sessionmodule.First,weaddthistoourapplication:
>npminstallexpress-session--save
Wealsoneedsomewheretostoresessiondata.Expresssupportsavarietyofsessionstoresviaadditionalmodules.RedisiswellsuitedtothistaskandwealreadyhaveaRedisinstanceavailable.Wecanusetheconnect-redismoduletostoresessionsinRedis:
>npminstallconnect-redis--save
Wecannowcreateanewconfigurationmoduletokeepalloursessionlogicinoneplace.Sincethiswillreturnmiddleware,we'llputitinthemiddlewarefolderheresrc/middleware/sessions.js:
'usestrict';
constsession=require('express-session');
letconfig={
secret:process.env.SESSION_SECRET,
saveUninitialized:false,
resave:false
};
if(process.env.REDIS_URL&&process.env.NODE_ENV!=='test'){
constRedisStore=require('connect-redis')(session);
config.store=newRedisStore({url:process.env.REDIS_URL});
}
module.exports=session(config);
WeconfiguretheExpresssessionmoduleasfollows:
UsethevalueofanenvironmentvariableasthesessionsecretOnlysavesessionsthatcontainsomedataDonotresavesessionsunlesstheyhavechangedIfRedisisavailable,useitasthesessionstore
Let'sconsidereachoftheconfigurationpropertiesinturn.
SpecifyingasessionsecretExpressusesasessionsecrettoprotectsessiondatafrombeingtamperingwith.YoushouldspecifythisbysettingtheSESSION_SECRETenvironmentvariablelocally.Thevalueisarbitraryandcanbeanything,aslongasit'snotempty.WealsoneedtospecifythisinourintegrationtestsoitcanrunontheCIserver.Thefollowingcodeisfromgulpfile.js:
gulp.task('integration-test',...,(done)=>{
constTEST_PORT=5000;
process.env.SESSION_SECRET=
process.env.SESSION_SECRET||'testOnly';
require('./src/server.js').then((server)=>{
...
});
});
DecidingwhenthesessiongetssavedAvoidingunnecessarysavesisaminoroptimizationandcanavoidcertainraceconditions.Onlysavinginitializedsessionsallowsyoutorequestuserconsentbeforestoringanycookies.Thismightbenecessaryforcompliancewithregionallaws,mostnotablyintheEU.Seehttps://www.cookiechoices.org/formoreinformation.
UsingalternativesessionstoresBydefault,Expresswilluseanin-memorysessionstore.Thisisfinefordevelopmentpurposesandintestenvironmentswhereweonlyhaveoneapplicationprocess,butisnotsuitableforproductionuse.StoringsessionsoutofprocessinRedisisimportantifwewanttoscaleacrossmultipleinstances.WeconfiguretheRedisstorewithourexistingRedisURL.
Note
Inpractice,youmightwanttousedifferentRedisinstancesforsessiondataandotherapplicationdata.Thesearequitedifferentusecases,sotheymightbenefitfromadifferentconfigurationofRedis.Forexample,sessiondataislikelytobehigherload,butcanaffordtobemorevolatile.Forsmall-scaleapplicationssuchasourexampleapplicationinthisbook,asingleRedisinstancewillsuffice.
UsingsessionmiddlewareWecannowusesessionselsewhereinourapplicationinsteadofdirectlysettingcookies.Thefollowingcodeisfromsrc/app.js:
letsessions=require('./middleware/sessions');
...
app.use(bodyParser.urlencoded({extended:false}));
app.use(sessions);
app.use(express.static(path.join(__dirname,'public')));
...
Thefollowingcodeisfromsrc/middleware/users.js:
'usestrict';
module.exports=(service)=>{
constuuid=require('uuid');
returnfunction(req,res,next){
letuserId=req.session.userId;
if(!userId){
userId=uuid.v4();
req.session.userId=userId;
req.user={
id:userId
};
next();
}else{
...
}
};
};
Thefollowingcodeisfromsrc/server.js:
'usestrict';
module.exports=require('./config/mongoose').then(mongoose=>{
...
io.use(adapt(require('./middleware/sessions')));
constusersService=require('./services/users.js');
...
});
ImplementingsocialloginForourfirstexample,we'lluseTwitterasourthird-partyauthenticationprovider.IfyouwanttofollowalongwiththeexampleyouwillneedaTwitteraccount,whichisveryquicktosetup.
SettingupaTwitterapplicationInorderforTwittertorecognizeourapplication,weneedtocreateanewappinTwitter'sdeveloperportal:
1. Visithttps://apps.twitter.com/andclickonCreateNewApp.2. FillintheName,Description,Website,andCallbackURLfields:
Ifyou'vedeployedyourapplicationtoHeroku,youcanuseitsHerokuURLhereOtherwise,justfillinplaceholdervaluesforbothfields(forexample,http://test.example.com/callback)
3. ClickonCreateyourTwitterapplication.4. ClickontheSettingstabandensurethatEnableCallbackLockingisunchecked(leaving
thisuncheckedallowsyoutouseplaceholdervaluesfortheURLsandisalsousefulforlocaltesting).
5. ClickontheKeysandAccessTokenstabtoviewyourapplication'sConsumerKey(APIKey)andConsumerSecret(APISecret).
SetnewlocalenvironmentvariablesnamedTWITTER_API_KEYandTWITTER_API_SECRET,containingthecorrespondingvaluesfromTwitter.YoumightwanttocreateashellscriptorbatchfiletosettheseintheconsoleorconfigurethemasHerokuenvironmentvariables(seeChapter11,DeployingNode.jsApplications)
ConfiguringPassportWe'llnowmakeuseofPassporttoallowuserstologintooursiteviaTwitter.First,weneedtoinstalltherelevantnpmpackages:
>npminstallpassport--save
>npminstallpassport-twitter--save
NowwecanconfigurePassporttoauthenticatewithTwitter.Weaddthefollowingcodeundersrc/config/passport.js:
'usestrict';
constpassport=require('passport');
constTwitterStrategy=require('passport-twitter').Strategy;
module.exports=(usersService)=>{
if(process.env.TWITTER_API_KEY&&
process.env.TWITTER_API_SECRET){
passport.use(newTwitterStrategy({
consumerKey:process.env.TWITTER_API_KEY,
consumerSecret:process.env.TWITTER_API_SECRET,
callbackURL:'/auth/twitter/callback',
passReqToCallback:true
},(req,token,tokenSecret,profile,done)=>{
usersService.setUsername(req.user.id,
profile.username||profile.displayName)
.then(()=>{done();},done);
}));
}
returnpassport;
};
ThisusestheTwitterStrategyforauthenticationwithTwitter,passinginourAPIkeyandsecretonaconfigurationobject.ThesecondconstructorparameterisafunctionthatPassportwillinvokeafterauthenticatingwithTwitter(referredtoastheverifycallbackinPassport'sdocumentation).Herewesetthecurrentuser'snamebasedontheprofile.usernameorprofile.displayNameprovidedfromTwitterbyPassport.
Note
Theprofileobjectcontainstheuserprofilereturnedbytheauthenticationprovider.Passportstandardizesprofiledatatomakeiteasiertoworkwithmultiplestrategies.There'sastandardsetoffields,suchasdisplayName,whichallPassportstrategieswillpopulateifpossible.We'dprefertousetheTwitterusername(forexample,hgcummings)thanthedisplayname(forexample,HarryCummings).Theprofile.usernamefieldcontainstheTwitterusername.Thisisnotoneofthestandardfields,butmanystrategieswillreturnafieldwiththisname.Soweuseprofile.usernamefirst,butfallbacktothemorestandardprofile.displayName.
NowwejustneedtomakeuseofournewpassportmoduleinExpress.Thefollowingcodeis
fromsrc/app.js:
letpassport=require('./config/passport')(usersService);
...
app.use(users);
app.use(passport.initialize());
app.post('/auth/twitter',passport.authenticate('twitter'));
app.get('/auth/twitter/callback',
passport.authenticate('twitter',
{successRedirect:'/',failureRedirect:'/'}));
app.use('/',routes);
...
Thistellsourapplicationtodothreethings:
UsePassport'sExpressmiddlewareAuthenticateusersviaTwitterwhentheyPOSTto/auth/twitterHandleTwitterauthenticationresultsat/auth/twitter/callbackbeforeredirectinguserstothehomepage
Finally,weneedtoprovidealoginbuttontoreachournewendpointasshownhereinsrc/views/index.js:
<h1>{{title}}</h1>
<h2>Account</h2>
{{#ranking}}
...
{{/ranking}}
<formaction="/auth/twitter"method="POST">
<inputtype="submit"value="LoginusingTwitter"/>
</form>
<h3>Profile</h3>
<formaction="/profile"method="POST">
...
</form>
...
IfyouruntheapplicationandclickLoginusingTwitter,thefollowingwillhappen:
TheapplicationwillredirectyourbrowsertoTwitterTwitterwillpromptyoutologinifyouhavenotalreadyTwitterwillaskwhetheryou'rehappywiththeapplicationseeingyourprofiledetailsandotherpublicdataTwitterwillthenredirectyourbrowsertothe/auth/twitter/callbackendpointYourbrowserwillmakearequesttothisendpointwithyourauthenticationtokenfromTwitterPassportwillvalidatethistokentheninvokeourloginhandlerfunctionWhenourfunctioncompletes,Passportwillreturnaredirectresponsetothehomepage
WehavenowintegratedTwitterauthenticationwithourapplication!However,we'renotreally
usingittoallowuserstologin.We'rejustassociatingaTwitterusernamewithourexistinguserIDscreatedforeachsession.Youcanseethisbyopeninguptwoseparatebrowsersessions.Trylogginginwitheachofthem.Ifyoucreateanewgameinonebrowser,itappearsintheotherbrowserinthelistofgamescreatedbyotherusers.ThisisbecauseyounowhavetwouserIDsassociatedwiththesameTwitterusername.
WeneedtorecognizethesameuserwhenevertheyloginwiththesameTwitteraccount.Thisshouldnotdependonbeinginthesamebrowsersession.Toaddressthis,we'llneedtodothefollowing:
PersistuseraccountstoourdatabaseTellPassporthowtostoreandretrieveusersLetPassportassociateauserwiththecurrentsession
PersistinguserdatawithRedisWealreadyuseRedistoassociateusernameswithuserIDs.NowwewanttobeabletoassociateuserIDswithTwitteraccountsaswell.Thefirsttimeauserlogsinwithanexternalprovider,wewanttocreateanewuserwiththenametakenfromtheexternalprofile.Subsequentrequestsauthenticatedwiththesameproviderwillseethesameuser.
WecanimplementthisfunctionalityusingRedis'sSETNXoperation.Thiswillonlysetakeyifitdoesnotalreadyexistandreturnwhetherthiswasthecase.Ourimplementationisasfollowsfromsrc/services/users.js:
'usestrict';
constredisClient=require('../config/redis.js');
constuuid=require('uuid');
constgetUser=userId=>
redisClient.getAsync(`user:${userId}:name`)
.then(userName=>({
id:userId,
name:userName
}));
constsetUsername=(userId,name)=>
redisClient.setAsync(`user:${userId}:name`,name);
module.exports={
getOrCreate:(provider,providerId,providerUsername)=>{
letproviderKey=`provider:${provider}:${providerId}:user`;
letnewUserId=uuid.v4();
returnredisClient.setnxAsync(providerKey,newUserId)
.then(created=>{
if(created){
returnsetUsername(newUserId,providerUsername)
.then(()=>getUser(newUserId));
}else{
returnredisClient
.getAsync(providerKey).then(getUser);
}
});
},
getUser:getUser,getUsername:userId=>
redisClient.getAsync(`user:${userId}:name`),
setUsername:setUsername,
...
};
Here,wecreateanewuserIDandtellRedistoassociateitwiththeexternalprovider(forexample,Twitter)account.Ifwehaveseentheexternalaccountbefore,wereturntheuserthatwasalreadyassociatedwithit.Otherwise,wepersistanewuserIDandassociateitwiththeusernamefromtheexternalprofile.Testsforthisfunctionalitycanbefoundinthecompanioncode.
ConfiguringPassportwithpersistenceNowthatwehaveawayofpersistingusers,weneedtotellPassporthowtomakeuseofthis.First,weupdateourverifycallbacktomakeuseofournewgetOrCreatefunctionratherthanjustsettingausername.ThenweneedtotellPassporthowtoidentifyandretrieveusersassociatedwithasessionbyserializinguserstoandfromastring.Thefollowingcodeisfromsrc/config/passport.js:
'usestrict';
constpassport=require('passport');
constTwitterStrategy=require('passport-twitter').Strategy;
module.exports=(usersService)=>{
if(process.env.TWITTER_API_KEY&&
process.env.TWITTER_API_SECRET){
passport.use(newTwitterStrategy({
consumerKey:process.env.TWITTER_API_KEY,
consumerSecret:process.env.TWITTER_API_SECRET,
callbackURL:'/auth/twitter/callback',
passReqToCallback:true
},(req,token,tokenSecret,profile,done)=>{
usersService.getOrCreate('twitter',profile.id,
profile.username||profile.displayName)
.then(user=>done(null,user),done);
}));
}
passport.serializeUser((user,done)=>{
done(null,user.id);
});
passport.deserializeUser((id,done)=>{
usersService.getUser(id)
.then(user=>done(null,user))
.catch(done);
});
returnpassport;
};
Passportstoresthestringversionoftheuser(returnedbyourserializeUsercallback)onthesession.ItusesourdeserializeUsercallbacktoturnthisstringintoauserobjectwhichitaddstotherequest.Inourcase,thestringrepresentationoftheuserisjusttheirIDanddeserializationisjustalookupintheusersservice.
Inorderforthistowork,wealsoneedtotellourapplicationtousePassport'sownsessionmiddleware,whichworkstogetherwithExpresssessions.Toavoidrepetition,we'llspecifyallofoursession-relatedmiddlewareinoursessionmiddlewaremodule.Thefollowingisthecodefromsrc/middleware/sessions.js:
...
constexpressSession=session(config);
module.exports=passport=>[
expressSession,passport.initialize(),passport.session()
];
Thismodulenowreturnsthreemiddlewareinstances.WewanttousethiswithbothExpressandSocket.IO.Thefirstoftheseissimple,sincewecanpassmultiplemiddlewareobjectstotheExpressapp.usefunctionasheresrc/app.js:
...
letpassport=require('./config/passport')(usersService);
letsessions=require('./middleware/sessions')(passport);
...
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended:false}));
app.use(sessions);
app.use(express.static(path.join(__dirname,'public')));
app.post('/auth/twitter',passport.authenticate('twitter'));
...
ForSocket.IO,weneedtoadapteachmiddlewareinturnasheresrc/server.js:
...
constusersService=require('./services/users.js');
letpassport=require('./config/passport');
require('./middleware/sessions')(passport).forEach(
middleware=>io.use(adapt(middleware)));
require('./realtime/chat')(io);
...
Notethat,inbothcases,ourusersmiddlewareisnolongerneededandcannowbedeleted.However,thismiddlewarepreviouslyensuredthattherewasalwaysauserobjectontherequest.Thiswillnowonlybethecasewhenthereisaloggedinuser,soweneedtoupdatetherestofourapplicationaccordingly.
Thereareafewplacesinourapplicationthatassumetherewillalwaysbeauserontherequest.Sincethisisnolongerguaranteed,therearetwowaystoresolvethis:wecanupdateourcodetocopewithnouserbeingpresentontherequestorwecanhidefunctionalityfromunauthenticatedusers.
Westillwantunauthenticateduserstobeabletoviewpublicchatandtoseeandplaygames,soweupdatethisfunctionalityaccordingly.Thecodefromsrc/realtime/chat.jsisupdatedasfollows:
namespace.on('connection',(socket)=>{
letusername=null;
if(socket.request.user){
username=socket.request.user.name;
}
...
Thefollowingcodeisfromsrc/realtime/games.js:
functionforwardEvent(name,socket){
service.events.on(name,game=>{
if(!socket.request.user||
game.setBy!==socket.request.user.id){
socket.emit(name,game.id);
}
});
}
Thefollowingcodeisfromsrc/routes/games.js:
router.post('/:id/guesses',function(req,res,next){
checkGameExists(
req.params.id,
res,
game=>{
if(req.user&&game.matches(req.body.word)){
userService.recordWin(req.user.id);
}
...
},
next
);
});
HidingfunctionalityfromunauthenticatedusersWecertainlywantunauthenticateduserstobeabletovisitthehomepageofourapplication,butmightnotwanttodisplayalloftheapplication'sfunctionalitytothem.Toachievethis,we'llupdateourindexrouteasfollowsfromsrc/routes/index.js:
router.get('/',function(req,res,next){
letuserId=null;
if(req.user){
userId=req.user.id;
}
Promise.all([gamesService.createdBy(userId),
gamesService.availableTo(userId),
usersService.getUsername(userId),
usersService.getRanking(userId),
usersService.getTopPlayers()])
.then(results=>{
res.render('index',{
title:'Hangmanonline',
loggedIn:req.isAuthenticated(),
createdGames:results[0],
...
});
})
.catch(next);
});
NotethatthisaddsaloggedInpropertytotheviewdatainsteadoftheuserID.ThevalueofthispropertycomesfromtheisAuthenticatedfunction,whichisaddedtotherequestbyPassport.Weusethistohidefeaturesthatwillnolongerworkforunauthenticatedusersandhidetheloginbuttonfromauthenticatedusers.Thefollowingcodeisfromsrc/views/index.hjs:
...
<body>
...
{{^loggedIn}}
<formaction="/auth/twitter"method="POST">
<inputtype="submit"value="LoginusingTwitter"/>
</form>
{{/loggedIn}}
{{#loggedIn}}
<h3>Profile</h3>
<formaction="/profile"method="POST">
...
</form>
{{/loggedIn}}
<h2>Games</h2>
{{#loggedIn}}
<formaction="/games"method="POST"id="createGame">
...
</form>
<h3>Gamescreatedbyyou</h3>
...
{{/loggedIn}}
<h3>Gamesavailabletoplay</h3>
...
<h2>Topplayers</h2>
...
<h3>Lobby</h3>
<formclass="chat"data-room="lobby">
<divid="messages"></dl>
{{#loggedIn}}
<inputid="message"/><inputtype="submit"value="Send"/>
{{/loggedIn}}
</form>
</body>
</html>
IntegrationtestingwithPassportWestillhaveoneproblem,whichisthatourintegrationtestswon'tworkanymore.Onlylogged-inuserscancreategamesnow.ItwouldbeagoodideatowriteanewintegrationtesttocheckthatTwitterauthenticationworks.Wedon'twanttointroduceaTwitteraccountdependencytoourcurrenttestthough.
Instead,we'llmakeuseofthepassport-localstrategytoallowourtesttologin.We'llinstallthisasadevdependencysoitcan'taccidentallyruninproduction:
>npminstallpassport-local--save-dev
WeconfigurePassporttoacceptanyusernameandpassword.Ifusingpassport-localforreal,thisiswhereyouwouldcheckagainstcredentialsinyourdatastore.Thefollowingcodeisfromsrc/config/passport.js:
if(process.env.NODE_ENV==='test'){
constLocalStrategy=require('passport-local');
constuuid=require('uuid');
passport.use(newLocalStrategy((username,password,done)=>{
constuserId=uuid.v4();
usersService.setUsername(userId,username)
.then(()=>{
done(null,{id:userId,name:username});
});
}
));
}
Thenweaddanewlocalauthenticationendpointtoourapplicationasheresrc/app.js:
if(process.env.NODE_ENV==='test'){
app.post('/auth/test',
passport.authenticate('local',{successRedirect:'/'}));
}
Andfinallyupdateourtesttologinasafirststepascodefromintegration-test/game.jsshownfollows:
functionwithGame(word,callback){
page.open(rootUrl+'/auth/test',
'POST',
'username=TestUser&password=dummy',
function(){
...
}
);
}
AllowinguserstologoutUserswillalsoexpectustoprovideawaytologoutofourapplication.Passportmakesthiseasybyaddingalogoutfunctiontotherequest.Wejustneedtomakeuseofthisinoneofourroutesheresrc/routes/index.js:
router.post('/logout',function(req,res){
req.logout();
res.redirect('/');
});
Wecanaddalogoutbuttontoourviewtomakeuseofthisnewrouteasinsrc/views/index.hjs:
{{#loggedIn}}
<formaction="/logout"method="POST">
<inputtype="submit"value="Logout"/>
</form>
<h3>Profile</h3>
AddingotherloginprovidersNowthatwehaveallthegeneralinfrastructureforauthentication,addingadditionalprovidersiseasy.Let'saddFacebookauthenticationasanexample.First,weneedtoinstalltherelevantPassportstrategy:
>npminstallpassport-facebook--save
ThenwecanupdateourPassportconfigfilefromsrc/config/passport.jsasfollows:
...
constFacebookStrategy=require('passport-facebook').Strategy;
module.exports=(usersService)=>{
constproviderCallback=providerName=>
function(req,token,tokenSecret,profile,done){
usersService.getOrCreate(providerName,profile.id,
profile.username||profile.displayName)
.then(user=>done(null,user),done);
};
if(process.env.TWITTER_API_KEY&&
process.env.TWITTER_API_SECRET){
passport.use(newTwitterStrategy({
consumerKey:process.env.TWITTER_API_KEY,
consumerSecret:process.env.TWITTER_API_SECRET,
callbackURL:'/auth/twitter/callback',
passReqToCallback:true
},providerCallback('twitter')));
}
if(process.env.FACEBOOK_APP_ID&&
process.env.FACEBOOK_APP_SECRET){
passport.use(newFacebookStrategy({
clientID:process.env.FACEBOOK_APP_ID,
clientSecret:process.env.FACEBOOK_APP_SECRET,
callbackURL:'/auth/facebook/callback',
passReqToCallback:true
},providerCallback('facebook')));
}
...
};
Herewe'vegeneralizedourverifycallbackfunctiontotakedifferentprovidernames,thenusedthiswithbothTwitterandFacebookauthenticationstrategies.Wecanre-usethistoaddfurtherstrategiesinthesameway.Wejustneedtosettherelevantenvironmentvariablesforthemtowork.
Note
ToobtainaFacebookAppIDandSecret,createanewFacebookapplicationathttps://developers.facebook.com/apps/(whichrequiresyoutohaveaFacebookaccount).Thisis
verysimilartotheprocessforTwitter.JustcreateanewapplicationoftypeWebsite,withaURLthatmatchesyourdevelopmentenvironment(forexample,http://localhost:3000).Oncecreated,theAppIDandAppSecretwillbevisibleontheDashboardpagefortheapplication.
WealsoneedtoaddFacebookauthenticationroutestoourapplicationconfigfile.ThesearejustthesameasthecorrespondingTwitterroutes.AswiththePassportconfigfile,wecancommonizebyparameterizingtheprovidername.Thecodefromsrc/app.jsisasfollows:
app.use(sessions);
constaddAuthEndpoints=provider=>{
app.post(`/auth/${provider}`,passport.authenticate(provider));
app.get(`/auth/${provider}/callback`,
passport.authenticate(provider,{successRedirect:'/',
failureRedirect:'/',session:true}));
};
addAuthEndpoints('twitter');
addAuthEndpoints('facebook');
Finally,weneedtoaddabuttontoallowuserstologinwithFacebook.Thefollowingcodeisfromsrc/views/index.hjs:
{{^loggedIn}}
<formaction="/auth/twitter"method="POST">
<inputtype="submit"value="LoginusingTwitter"/>
</form>
<formaction="/auth/facebook"method="POST">
<inputtype="submit"value="LoginusingFacebook"/>
</form>
{{/loggedIn}}
Addingadditionalprovidersiseasy.ToaddGoogle+authentication,wewouldjustneedtofollowthesesteps:
1. Installthepassport-googlenpmmodule2. Createanewapplicationasdescribedat
https://developers.google.com/identity/protocols/OpenIDConnect3. Updatethethreefileslistedabove,passingtheGoogleprovidertoournewcommon
functions
SummaryInthischapter,wehaveaddedauthenticationtoourExpressapplicationusingPassport,introducedExpresssessionsusingRedisforsessionstorage,leveragedmultiplePassportstrategiestosupportdifferentexternalproviders,andpersisteduserdatainRedis.
Thiscompletesourexamplewebapplication.InthenextchapterwewilllookathowtocreatedifferentkindsofNode.jsproject:alibraryandacommand-linetool.
Chapter13.CreatingJavaScriptPackagesSofarwehavebuiltupawebapplication,makinguseofvariousnpmpackagesalongtheway.ThesepackagesincludelibrariessuchasExpressandcommand-linetoolssuchasGulp.Nowwe'lllookathowtogoaboutcreatingpackagesofourown.
Inthischapterwewill:
ExplorethedifferentmodulesystemsavailableforJavaScriptCreateourownJavaScriptlibraryWriteJavaScriptthatcanrunonboththeclientandserver-sideCreateacommand-linetoolinJavaScriptReleaseanewnpmpackageUseNode.jsmodulesinthebrowserenvironment
Note
Thecodeexamplesinthischapterareindependentofeverythingwe'vedonesofar.
WritinguniversalmodulesWehavealreadywrittenmanyofourownmodulesaspartofourapplication.Wecanalsowritelibrarymodulesforuseinotherapplications.
Whenwritingcodeforusebyothers,it'sworthconsideringinwhatcontextsitwillbeuseful.Somelibrariesareonlyusefulinspecificenvironments.Forexample,Expressisserver-specificandjQueryisbrowser-specific.Butmanymodulesprovidefunctionalitythatwouldbeusefulinanyenvironment,forexample,utilitymodulessuchastheuuidmodulewe'veusedelsewhereinthisbook.
Let'slookatwritingamoduletoworkinmultipleenvironments.We'llneedtosupportmorethanjustNode.js-stylemodules.We'llalsoneedtosupportclient-sidemodulesystemssuchasRequireJS.RecallfromChapter4,IntroducingNode.jsModules,thatNode.jsandRequireJSimplementtwodifferentmodulestandards(CommonJSandAsynchronousModuleDefinition(AMD),respectively).Ourpackagemayalsobeusedclient-sideinawebsitewithnomodulesysteminplace.
Asanexample,let'screateamoduleprovidingasimpleflatMapmethod.ThiswillworklikeSelectManyin.NET'sLINQ.Itwilltakeanarrayandafunctionthatreturnsanewarrayforeachelement.Itwillreturnasinglearrayofthecombinedresults.
AsaNode.js/CommonJSmodule,wecouldimplementthisasfollows:
module.exports=functionflatMap(source,callback){
returnArray.prototype.concat.apply([],source.map(callback));
}
ComparingNode.jsandRequireJSRecallfromChapter4,IntroducingNode.jsModules,thateachmodulesystemprovidesthefollowing:
AwayofdeclaringamodulewithanameanditsownscopeAwayofdefiningfunctionalityprovidedbythemoduleAwayofimportingamoduleintoanotherscript
Node.jsimplementstheCommonJSmodulestandard.Modulenamescorrespondtofilepathsandeachfilehasitsownscope.Modulesdefinethefunctionalitytheyprovideusingtheexportsalias.Modulesareimportedusingtherequirefunction.
RequireJSisdesignedforthebrowserenvironment.Inthebrowserthereisnonewscopeperfile(allscriptfilesexecuteinthesamescopeandcanseethesamevariables).Also,modulesmustbeloadedbynetworkrequestsratherthanfromthelocalfilesystem.
RequireJSimplementstheAMDstandard.AMDspecifiestwofunctions,whichRequireJSaddstothetop-levelwindowobjectinthebrowserenvironment:
Thedefinefunctionallowsnewmodulestobecreatedbyprovidinganameandafactoryfunctionforthemodule.Thescopeofthemodulewillbethescopeofitsfactoryfunction.Thefunctionalityofthemoduleisdefinedbythereturnvalueofthefactoryfunction.Therequirefunctionallowsmodulestobeimported.AlthoughthishasthesamenameasthemoduleimportfunctioninNode.js,itworksverydifferently.Multiplemodulenamescanbespecifiedforimport(asanarray).Therequirefunctionisasynchronousandtakesacallbacktobeexecutedwhenallthedependenciesareloaded.ThisallowsRequireJStoloadmodulesefficientlyinthebrowserenvironment.
SupportingthebrowserenvironmentForourmoduletoworkinthebrowserenvironment,weneedtosupporttheAMDstandardsoRequireJScanwork.Wealsoneedtoaccommodatesitesnotusinganymoduleloader.Wecanachievethisbyextendingourmoduledefinitionasfollows,inscripts/flatMap.js:
(function(root,factory){
'usestrict';
if(typeofdefine==='function'&&define.amd){
define([],factory);
}elseif(typeofmodule==='object'&&module.exports){
module.exports=factory();
}else{
root.flatMap=factory();
}
}(this,function(){
'usestrict';
returnfunctionflatMap(source,clbk){
returnArray.prototype.concat.apply([],source.map(clbk));
}
}));
Note
Notetheuseofananonymousfunctionthatisinvokedstraightaway,calledanImmediately-InvokedFunctionExpression(IIFE).ThisisacommonwayofcreatinganisolatedscopeinJavaScriptenvironmentswithoutbuilt-inmodules.
First,wecheckfortheexistenceofanAMD-styledefinefunction(theexistenceofadefine.amdpropertyisalsospecifiedbytheAMDstandard).Notethattheasynchronousnatureofthedefinefunctionmeansthatweneedtouseafactoryfunctiontocreateourmodule.Weprovidealistofdependencies(emptyinthiscase)andourfactoryfunctiontothedefinefunctiontocreateourmodule.
IfnoAMDmodulesystemispresent,wecheckfortheCommonJS-stylemodule.exportsusedbyNode.js.Finally,ifneithermodulesystemispresent,weprovideourmoduleasapropertyontherootparameter.Ourargumentforthisparameteristhethiskeywordevaluatedintheglobalscope.Inabrowser,thiswillbethewindowobject.
UsingAMDmoduleswithRequireJSLet'screateasimplewebpagetocheckthatourmoduleworkscorrectlywithRequireJS.We'llalsoshowhowtouseRequireJSwithanexternallibrary,jQuery.
FirstwedefineanHTMLfileforthepage:
<!DOCTYPEhtml>
<html>
<head>
<scriptdata-main="scripts/main"
src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.22/require.min.js"
></script>
<style>input,pre{display:block;margin:0.5emauto;width:320px;
}</style>
</head>
<body>
<inputtype="text"/>
<inputtype="text"/>
<inputtype="text"/>
<inputtype="text"/>
<preid="wordcounts"></pre>
</body>
</html>
NotethattheonlyscripttagonthepageisforRequireJSitself.Thisscripttagalsohasadataattributeindicatingtheentrypointofourapplication.Thepathscripts/maintellsRequireJStoloadscripts/main.js,whichcontainsthefollowing:
requirejs.config({
paths:{
jquery:
'https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.1/jquery.min'
}
});
require(['flatMap','jquery'],function(flatMap,$){
$('input').change(function(){
varallText=$.map($('input'),function(input){
return$(input).val();
}).filter(function(text){
return!!text;
});
varallWords=flatMap(allText,function(text){
returntext.split('');
});
varcounts={};
allWords.forEach(function(word){
counts[word]=(counts[word]||0)+1;
});
$('#wordcounts').text(JSON.stringify(counts));
})
});
ThisscriptfirstconfiguresRequireJS.Theonlyconfigpropertyspecifiedhereisthepathproperty.ThepathforjQueryunderthekey'jquery'tellsRequireJShowtoresolvethe'jquery'dependency.Wedon'tneedtospecifyapathforflatMap.jsbecausewehavesaveditunderthesamedirectoryasmain.js.
NextweusetherequirefunctiontoloadflatMapandjQueryandpassthemintoourmainapplicationfunction.InlargerapplicationsusingRequireJS,thisisusuallyaveryshortbootstrapfunction.Themain.jsfileisalsooftentheonlyplacethatyou'llseearequirecall.Mostoftheapplicationcodeisinmodulesdeclaredwithdefine.
AsthisisjustatestofourlibrarywithRequireJS,we'llputtherestofourapplicationcodeinsideourmainapplicationfunction.WeuseourflatMapmoduleandjQuerytocalculateanddisplaywordcountsacrossallthetextinputs.Youcanseethisworkingbyopeningindex.htmlinyourbrowser:
IsomorphicJavaScriptTheflatMap.jsexampleaboveisanimplementationoftheUniversalModuleDefinitionpattern.Seehttps://github.com/umdjs/umdforannotatedtemplatesforthispattern.Thesetemplatesalsoshowhowtodeclaredependenciesbetweenmodulesthatfollowthispattern.
Moregenerally,writingcodethatachievesthesameresultbothontheserverandinthebrowserisreferredtoasIsomorphicJavaScript.Seehttp://isomorphic.net/formoreexplanationandexamplesofthisprinciple.
WritingnpmpackagesIfyoucreatesomecodethatwouldbeusefultoothers,youcandistributeitasannpmpackage.Todemonstratethis,we'llimplementsomeslightlymorecomplexfunctionality.
Note
Youcanfindtheexamplecodeforthissectionathttps://github.com/NodeJsForDevelopers/autotoc.Notethat,unlikepreviouschapters,thereisnotonepercommitperheading.Thelistingsintherestofthissectionmatchthefinalversionofthecode.
We'regoingtoimplementatoolforgeneratingatableofcontents(ToC)bycrawlingawebsite.Tohelpwiththis,we'llmakeuseofafewothernpmpackages:
requestprovidesanAPIformakingHTTPrequests,whichishigher-levelandmuchsimplertousethanthebuildintheNode.jshttpmodulecheerioprovidesjQuery-likeHTMLtraversaloutsideofthebrowserenvironmentdenodeify,mentionedinChapter8,MasteringAsynchronicity,allowsustousetherequestlibrarywithpromisesinsteadofcallbacks
Tip
It'scommonfornpmpackagestodependonotherpackagesinthisway.Butitisworthminimizingyourpackage'sdependenciesifyouwantittobeappealingtootherdevelopers.Packageswithmanytransitivedependenciescanaddalotofbloattoapplications,andmakeitharderfordeveloperstobeconfidentthattheyunderstandeverythingtheyarepullingintotheirapplication.
Thecodeforourmodulefollows,asgiveninautotoc.js:
'usestrict';
constcheerio=require('cheerio');
constrequest=require('denodeify')(require('request'));
consturl=require('url');
classPage{
constructor(name,url){
this.name=name;
this.url=url;
this.children=[];
}
spider(){
returnrequest(this.url)
.then(response=>{
let$=cheerio.load(response.body);
letpromiseChildren=[];
$('a').each((i,elem)=>{
letname=$(elem).contents().get(0).nodeValue;
letchildUrl=$(elem).attr('href');
if(name&&childUrl&&childUrl!=='/'){
letabsoluteUrl=url.resolve(this.url,childUrl);
if(absoluteUrl.indexOf(this.url)===0&&
absoluteUrl!==this.url){
letchildPage=newPage(name.trim(),absoluteUrl);
if(childUrl.indexOf('#')===0){
promiseChildren.push(Promise.resolve(childPage));
}else{
promiseChildren.push(childPage.spider());
}
}
}
});
returnPromise.all(promiseChildren).then(children=>{
this.children=children;
returnthis;
});
});
}
}
module.exports=baseUrl=>newPage('Home',baseUrl).spider();
It'snotimportanttounderstandeverysinglelineaswe'remoreinterestedinhowitwillbepackaged.Theimportantpointsare:
WeloadthestartingpagethenfollowlinksthroughtootherpagesandprocesstheserecursivelytobuilduptheentireToCWeonlyfollowlinkstomorespecificURLsthanthecurrentpage(thatis,subpaths),sowedon'tgetintoinfiniteloopsAteachlevel,weloadallchildpagesinparallelandusePromise.alltocombinetheresults
We'llalsoaddasimplemoduletoprintaToCtotheconsole,asgiveninconsolePrinter.js:
'usestrict';
constprintEntry=function(entry,indent){
console.log(`${indent}-${entry.name}(${entry.url})`);
entry.children.forEach(childEntry=>{
printEntry(childEntry,indent+'');
})
}
module.exports=toc=>printEntry(toc,'');
DefiningannpmpackageTodefineannpmpackage,wemustaddafiletoactastheentrypointtoourpackage.Thiswilljustexposetheinnermodulesappropriately,asgiveninindex.js:
'usestrict';
module.exports=require('./autotoc.js');
module.exports.consolePrinter=require('./consolePrinter.js');
Wealsoneedtoaddannpmpackage.jsonfiletodefineourpackage'smetadata.Tocreatethisfile,youcanrunnpminitinthecommandlineandfollowtheprompts.Inourcase,theresultingfilelookslikethefollowing:
{
"name":"autotoc",
"version":"0.0.1",
"description":"Automatictableofcontentsgeneratorforwebsites",
"main":"index.js",
"author":"hgcummings<[email protected]>(http://hgc.io/)",
"repository":"https://github.com/NodeJsForDevelopers/autotoc",
"license":"MIT",
"dependencies":{
"cheerio":"^0.20.0",
"denodeify":"^1.2.1",
"request":"^2.69.0"
}
}
We'veusedpackage.jsonfilesbeforetospecifydependenciesfornpminstall.Theotherfieldsbecomemuchmoreimportantwhenpublishingapackagetonpm.Notethatweusethemainpropertytospecifyourpackage'sentrypoint.Actually,index.jsisthedefaultvalue,butspecifyingitexplicitlymakesthisclearer.
PublishingapackagetonpmOncewehavedefinedourpackage'smetadata,publishingittonpmisverystraightforward:
Ifyoudonotalreadyhaveannpmaccount,createonebyrunningnpmadduserandspecifyingausernameandpasswordLoginusingnpmloginIntherootfolderofthepackage,runnpmpublish
That'sallweneedtodo!Ourpackagewillnowappearintheglobalnpmrepository.Wecanmakeuseofitby(inanewfolder)runningnpminstallautotocandwritingthefollowingsimpledemoscriptasgivenindemo.js:
'usestrict';
constautotoc=require('autotoc');
autotoc('http://hgc.io')
.then(autotoc.consolePrinter,err=>console.log(err));
Runningnodedemo.jsatthecommandlineproducesthefollowingoutput:
RunningautomatedclientsonthewebIt'sfinetoruntoolslikethisagainstyourownwebsite.Therearemanyusecasesforthiskindoftechnique.Forexample,ascriptthatspidersthroughanentiresiteandcheckseverypagecanbeausefulintegration/smoketest.
Usecasesthatinvolvecrawlingsitesthatyoudon'townrequiremorecare.Anypublic-facingsitethatyoucouldvisitinabrowser,youcouldalsoaccesswithanautomatedclientlikethis.Butissuingalargenumberofautomatedrequestsagainstthesamehostisundesirable.ItcouldbeconsideredpooretiquetteatbestoraDenialofService(DoS)attackatworst.
ClientsshouldsetanappropriateUser-AgentHTTPheader.Someserversmightrejectrequestsfromclientsthatdon'tspecifyaUser-Agentordon'tappeartobeabrowser.Byconvention,crawlersshouldsendaUser-AgentincludingthewordbotinthenameandideallyaURLtofindoutmoreaboutthebot.Therequestlibrarymakesiteasytospecifyheadersbypassinginanoptionsobject.Forexample:
letoptions={
url:'http://hgc.io',
headers:{
'User-Agent':'Examplebot/1.0(+http://example.com/why-im-crawling-your-
website)'
}
};
request(options).then(...);
Crawlersshouldalsocheckforarobots.txtfileforeachwebsiteandrespectanyrulesitcontains.Seehttp://www.robotstxt.org/robotstxt.htmlformoreinformation.
Finally,legitimatecrawlersofthird-partywebsitesshouldalsorate-limittheirrequeststoavoidoverwhelmingtheserver.
ReleasingastandalonetooltonpmSomeofthenpmpackageswe'veusedsofarinthisbookhavebeencommand-linetoolsratherthanlibraries,forexampleGulp.Creatingacommand-linetoolpackageisverystraightforward.First,weneedtodefinethescriptthatwewantpeopletobeabletoinvokefromthecommandline,asgivenincli.js:
#!/usr/bin/envnode
'usestrict';
constautotoc=require('./autotoc.js');
constconsolePrinter=require('./consolePrinter.js');
autotoc(process.argv[2])
.then(consolePrinter,err=>console.log(err));
Thislooksmuchlikeourdemoscriptfrombefore,withacoupleofdifferences:
Thelineatthebeginningofthescript(calledashebangline,startingwith#!)indicatestotheOSthatthisscriptshouldbeexecutedusingNode.jsTheURLtocrawlistakenfromacommand-lineargument
Nowwejustneedtospecifythisscriptinourpackage.json:
{
"name":"autotoc",
"version":"0.1.1",
"description":"Automatictableofcontentsgeneratorforwebsites",
"main":"index.js",
"bin":{
"autotoc":"./cli.js"
},
"author":"hgcummings<[email protected]>(http://hgc.io/)","repository":
"https://github.com/NodeJsForDevelopers/autotoc",
"license":"MIT",
"dependencies":{
"cheerio":"^0.20.0",
"denodeify":"^1.2.1",
"request":"^2.69.0"
}
}
Topublishourupdatedpackage,wefirstneedtoupdateourversionnumber.Youcanupdatethisinthepackagedirectlyorusethenpmversioncommand,forexample
>npmversionminor
Thisautomaticallyupdatestheversionnumbertothenextmajor/minor/patchversion(asspecified)andmakesanewgitcommitwiththischange.
Sincewearealreadyloggedintonpm,wecannowpublishthenewversionofourpackagebyrunningnpmpublishagain.
WecannowmakeuseofourCLItoolasfollows(inanewcommandpromptwindow):
>npminstall-gautotoc
>autotochttp://hgc.io
UsingNode.jsmodulesinthebrowserAtthebeginningofthischapter,wediscussedcreatinguniversalmodulesthatcanrununderNode.jsorinthebrowser.Thereisanotherwaythatwecanallowourcodetoruninbothenvironments.
Browserify(http://browserify.org/)allowsyoutomakeuseofNode.jsmodulesinthebrowser.Itbundlesupyourcodetogetherwithitsdependencies.Italsoprovidesbrowser-compatibleshimstoemulateNode.jsbuilt-inmodules.
YoucaninstallBrowserifyvianpm:
>npminstall-gbrowserify
Browserifyistypicallyusedtopackageapplications.Forexample,ifwewantedtopackageourdemousageofautotocfromtheprevioussection,wecouldrun:
>browserifydemo.js-obundle.js
BrowserifywillcreateasingleJavaScriptfilecontainingthecodefromdemo.js,alongwithitsdependenciesandtransitivedependencies.IfweincludethisinanHTMLpage,wecannowseeitworkinginthebrowserconsole:
YoucanalsouseBrowserifytogeneratebrowser-compatiblefilesforindividualmodules,followingtheUniversalModuleDefinitionpatterndiscussedearlierinthischapter.Forexample,tocreateaUMDversionofourautotoc.jsmodulefromtheprevioussection,wecouldrun:
>browserifyautotoc.js-sautotoc-obrowser/scripts/autotoc.js
WecouldnowmakeuseofthisviaRequireJS.Let'screateasimpleapplicationthatusesautotoctogetherwithjQuerytogenerateanHTMLToC.Firstwe'llneedanHTMLfiletocontainourapplicationandincludeRequireJS,asgiveninbrowser/index.html:
<!DOCTYPEhtml>
<head>
<scriptdata-main="scripts/main"
src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.22/require.min.js"
></script>
</head>
<body>
</body>
Nowwecanimplementourapplicationitself,asgiveninbrowser/scripts/main.js:
requirejs.config({
paths:{
jquery:'https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.1/jquery.min'
}
});
require(['autotoc','jquery'],function(autotoc,$){
'usestrict';
autotoc('http://hgc.io').then(toc=>{
letprintEntry=function(entry,parent){
letlist=$(document.createElement('ul'));
list.append(
`<li><ahref="${entry.url}">${entry.name}</a></li>`);
entry.children.forEach(childEntry=>{
printEntry(childEntry,list);
})
parent.append(list);
}
printEntry(toc,$('body'));
},err=>console.log(err));
});
Thisresultsinthefollowingoutput:
ControllingBrowserify'soutputNotethat,bydefault,Browserifygeneratesabundleofyourcodeandallofitsdependencies.Includingtransitivedependencies,thiscanresultinaverylargefile.Theautotocmoduleisonly42lineslong,butthegeneratedbundleisover80,000lines!OurapplicationaboveincludesbothjQuery(viaRequireJS)andaversionofCheerio(viaBrowserify).Thisisparticularlywasteful,sincemuchofCheerioisare-implementationofjQuery.
YoucaninstructBrowserifytoexcludespecificmodulesandtoexcludeallexternalmodules.Thisisparticularlyusefulforthird-partymodulesthatfollowtheUMDpattern.Thesedonotneedtobebrowserifiedandcanbeexcludedfromthegeneratedbundle.Youcanthenloadthemseparatelyinthebrowser,viaanadditionalscripttagorusingRequireJS.
FormoreinformationonBrowserify'susageoptions,seetheofficialdocumentationathttps://github.com/substack/node-browserify#usage.
Browserifyprovidesalotofflexibilityforbundlingmodulesindifferentways.Itisparticularlyusefulwhenworkingonasinglecodebasewithbothserver-sideandclient-sidefunctionality.ItallowsyoutowriteallofyourcodeusingNode.js-stylemodulesandtoeasilysharemodulesbetweentheserverandtheclient.
SummaryInthischapter,wehavewrittenamulti-environmentmodulefollowingtheuniversalmoduledefinitionpattern,createdannpmpackageforalibraryandacommand-linetool,andpackagedNode.jscodeforthebrowserusingBrowserify.
ThisdemonstratestheflexibilityofNode.jsandtherangeofusecasesforJavaScriptandnpmbeyondjustserver-sidecode.Inthefinalchapter,we'lllookatthebroadercontextaroundNode.js.We'llseesomeofthenewerlanguagesandupcominglanguagefeaturesfortheplatformandhowNode.jsinteractswithotherplatformslike.NET.
Chapter14.Node.jsandBeyondSofar,thisbookhasshownyouhowtoworkwithJavaScriptandNode.jsinavarietyofusecases.Inthischapter,we'lllookathowtheJavaScriptecosystemiscontinuingtoevolve.We'llalsoseehowthe.NETandJavaScriptecosystemsinfluenceeachotherandhowtointegratethemwithinasingleproject.
WhilethechapterssofarhaveaimedtostartyouonyourpathintoNode.jsandJavaScript,thischapteraimstomapouttheremainingterritory.Eachoftheprecedingchaptershasprovidedin-depthstep-by-stepcoverageofasingletopic.Thischapterwillcoveramuchbroaderrangeoftopics,withlinkstoresourcesforfurtherreading.
Inthischapter,wewill:
UnderstandhowNode.jsandJavaScriptarecontinuingtoevolveIntroducesomeofthenewandupcomingJavaScriptlanguagefeaturesLookatsomealternativeprogramminglanguagesforNode.jsandthewebConsiderprinciplesfromNode.jsthatcanapplyto.NETprogrammingSeehowtointegrateNode.jswith.NET
UnderstandingNode.jsversioningAsmentionedinChapter1,WhyNode.js?,thereleaseofNode.jsv4in2015showstheplatformcomingtomaturity.Ifyou'veusedNode.jsbeforetheendof2015,youwouldhaveseenversionnumberssuchasv0.8.0orv0.12.0.Sowhytheleaptov4.0.0?
AbriefhistoryofNode.jsNode.jsisanopen-sourceprojectwithacorporatesponsor,Joyent.ThismeansthatasinglecompanyhasalotofinfluenceoverthedirectionofNode.js,butanyonecancreatetheirownforkofthesourcecode.Thisisexactlywhathappenedattheendof2014.AgroupofmajorcontributorstoNode.jssplittheprojecttocreateanewfork,namedio.js.Afewkeypropertiesofio.jswere:
AmoreopengovernancemodelAmoreregularreleasecycle,keepingmoreup-to-datewiththeunderlyingV8engine,totakeadvantageofperformanceimprovementsandnewerJavaScriptlanguagefeaturesAmovetosemanticversioning(seehttp://semver.org/),resultinginmajorversionnumbersincreasingmorequickly
Overthecourseof2015,theNode.jsprojectreshapeditselftotakeontheabovepropertiesandalignwithio.js.InSeptember2015,thereleaseofNode.jsv4broughtthetwoprojectsbacktogetherunderanewgovernancemodel.Node.jsv4supersedes(andmerges)bothNode.jsv0.12andio.jsv3.3.Youcanreadmoreaboutthenewgovernancemodelathttps://nodejs.org/en/about/governance/.
IntroducingtheNode.jsLTSscheduleThetimetableforNode.jsreleasesnowfollowsaregularschedule.Anewstablereleaseoccursevery6months.Eachstablebranchreceivesfixesaswellasnewfeaturesthatreachmaturity.Thelifetimeofstablereleasesalternatesasfollows(asshowninthefollowingchart):
Odd-numberedbrancheslivefor9monthsEven-numberedbranchesenterlong-termsupport(LTS)after6months,receivingbugfixesbutnonewfeaturesLong-termsupportlastsfor30months,withthefinal12monthsbeingmaintenancemode(criticalbugfixesonly)
YoucanfindmoredetailsoftheLTSmodelathttps://github.com/nodejs/LTS.
TheLTSmodelallowsyoutohaveconfidenceinNode.jsasaplatformforyourapplication.ThecodeinthisbooktargetsNode.jsv6,thecurrentstablereleaseatthetimeofpublication.ThisversionwillbeinLTSthroughtoApril2019,somethreeyearslater.
UnderstandingECMAScriptversioningECMAScriptistheformalstandardfortheJavaScriptlanguage.Thefirstthreeiterationsofthelanguageoccurredbetween1997and1999.A10-yeargapfollowedbeforeECMAScript5inDecember2009.ES5introducedfewnewfeaturesandfocusedoncleaningupthelanguage.Itintroducedstrictmodesandaddressedvariousinconsistencies,flaws,orgotchasinearlierversions.
2015sawamajorchangetothelanguageandtotheversioningapproach.ECMAScript2015(formerlyECMAScript6)introducedmanysignificantnewlanguagefeatures.Theseincludeclasses,let/constkeywordsandblock-scoping,arrowfunctions,andnativepromises.Intherestofthischapter,we'lllookatsomeoftheothersignificantnewfeaturesinES2015.
ThenamechangefromES6toES2015indicatesanewyearlyversioningmodel.From2015onwards,therewillbeanewversionoftheECMAScriptstandardeveryyear.Plannedfeaturesthataren'tquitereadyforreleasewillwaituntilthefollowingyear.Forthisreason,ECMAScript2016isasmallreleasewithonlyacoupleofnewfeatures.
NotethatECMAScriptisthestandardandittakestimefornewfeaturestobeimplemented.Indeed,someES2015featuresarestillmissingfromtheJavaScriptenginesinpopularbrowsers.NotethoughthatthemajorbrowservendorsarepartoftheECMAScriptstandardsprocess.Sobrowsers,andChrome'sV8engine(usedbyNode.js)inparticular,shouldgenerallynotlagtoofarbehindthelateststandard.
ExploringECMAScript2015WehavealreadyusedmanyofthenewfeaturesofES2015throughoutthisbook,suchasarrowfunctions,templatestrings,andpromises.WehavealsoalreadyseenES2015'ssyntaxforclassesinChapter3,AJavaScriptPrimer.
ES2015isamajorupdatetothelanguage,includingmanynewfeaturesandsyntaximprovements.Thissectionwillcoversomeoftheotherusefulimprovementsthatwehaven'tseensofarinthebook.ForcompletecoverageofeverythingnewinES2015,seetheexcellentExploringES6,availableathttp://exploringjs.com/es6/.
UnderstandingES2015modulesAsmentionedinpreviouschapters,ES2015introducesanewmodulespecification.RecallfromChapter4,IntroducingNode.jsModules,thateachmodulesystemprovidesthefollowing:
AwayofdeclaringamodulewithanameanditsownscopeAwayofdefiningfunctionalityprovidedbythemoduleAwayofimportingamoduleintoanotherscript
Modulesarescopedtotheircontainingfile,asinCommonJS.Modulesprovidefunctionalityviaanewexportkeyword.Prefixinganexpressionwithexportisequivalenttomakingitapropertyofthemodule.exportsvariableinCommonJS.Aspecialdefaultexportisequivalenttoassigningthevalueofmodule.exportsitself.Modulesareimportedusinganimportkeywordratherthanaspecialrequirefunction.Thereisoneadditionalrestriction:importsmustcomeatthetopofthescript,beforeanyconditionalblocksorotherlogic.
Thesemightseemlikesmallsyntaxchanges,buttheyhaveanimportantimplication.Becausedefiningandimportingmodulesdoesn'tinvolveassignmentandmethodcalls,thestructureofdependenciesbetweenmodulesisstatic.ThisallowstheJavaScriptenginetooptimizeloadingofmodules(particularlyimportantinthebrowser).Italsomeansthatcyclicdependenciesbetweenmodulescanberesolved.
YoucanfindoutmoreaboutthenewES2015modulesyntaxathttp://jsmodules.io/.
UsingsyntaximprovementsfromES2015Inthissectionwe'lllookatsomeofthenewsyntaxfeaturesinES2015thatwehaven'tusedinthebooksofar.TheseareallavailableinthelatestJavaScriptengines,includingNode.jsv6.
Thefor...ofloop
Let'ssaywehaveanarraydefinedasfollows:
letmyArray=[1,2,3];
Let'salsosaythatanotherlibraryhasaddedahelperfunctiontoallarrays.PerhapssomethinglikeourflatMapfunctionfromChapter13,CreatingJavaScriptPackages.
Array.prototype.flatMap=function(callback){
returnArray.prototype.concat.apply([],this.map(callback));
};
Ifyouwantedtoiteratethroughallthemembersofanarray,youmightbetemptedtouseJavaScript'sfor...inconstructasfollows:
for(letiinmyArray){
console.log(myArray[i]);
}
Thisdoesn'tworkverywellthough,asitincludespropertiesonthearray'sprototypeandprintsouttheflatMapfunctionaswellastheelementsinthearray.Thisisacommonproblemwithfor...inloops,whenusedwithobjectsaswellaswitharrays.Thestandardwaytoavoiditisbyskippingprototypepropertiesasfollows:
for(letiinmyArray){
if(myArray.hasOwnProperty(i)){
console.log(myArray[i]);
}
}
Thisprintsoutjusttheelementsofthearray,aswewant.Asimilarloopcouldalsobeusedtoprintthepropertiesofanobject,withoutaccidentallyattemptingtoprintoutfunctionsfromtheprototype(whichmayhavebeenaddedbyathird-partylibrary).
Notethatfor...inalsodoesn'ttechnicallyguaranteetheorderinwhichititeratesthroughthekeysofanobject.Thismeansit'snotreallythebestthingtousewitharrays,whereweexpectaspecificorder.That'swhythestandardwaytoiteratethrougharraysisusingaplainoldforloop,asfollows:
for(leti=0;i<myArray.length;++i){
console.log(myArray[i]);
}
ES2015addressestheseissueswithanewfor...ofloop,whichlookslikethis:
for(letvalueofmyArray){
console.log(value);
}
Thesyntaxisverysimilartofor...inloops.However,youdonotneedtofilteroutprototypemembersastheseareexcluded.Itcanbeusedwithanyiterableobjects(suchasarrays)andwillfollowthenaturalorderingoftheiterable.Inshort,for...ofloopsarelikefor...inloopsbutwithoutanynastysurprises.
Thespreadoperatorandrestparameters
Thespreadoperatorallowsyoutotreatarraysasiftheywereasequenceofvalues.Forexample,tocallafunction:
letmyArray=[1,2,3];
letmyFunc=(foo,bar,baz)=>(foo+bar)*baz;
console.log(myFunc(...values));//Prints9
Youcanalsousethespreadoperatorwithinarrayliterals,forexample:
letsubClauses=['2a','2b','2c'];
letclauses=['1','2',...subClauses,'3'];
//Equivalentto['1','2','2a','2b','2c','3']
Therestparametersyntaxservestheoppositepurpose,turningasequenceofvaluesintoanarray.ThisissimilartotheparamskeywordinC#orvarargsinJava.Forexample:
functionfoldLeft(combine,initial,...values){
letresult=initial;
for(letvalueofvalues){
result=combine(result,value);
}
returnresult;
}
console.log(foldLeft((x,y)=>x+y,0,1,2,3,4));//Prints10
Destructuringassignment
Destructuringallowsyoutousestructuringsyntaxtoassignmultiplevariablestogether.Forexample,youcanassignvariablesusingthearrayliteralsyntaxtodestructurearrays:
letfoo,bar;
[foo,bar]=[1,2];//Equivalenttofoo=1,bar=2
Youcanalsocombinedestructuringwiththespreadoperator:
[foo,bar,...rest]=[1,2,3,4,5];
//Equivalenttofoo=1,bar=2,rest=[3,4,5]
Finally,youcanusedestructuringwiththeobjectliteralsyntax:
{foo,bar}={foo:1,bar:2};//Equivalenttofoo=1,bar=2
Destructuringisparticularlyusefulfordealingwithcomplexreturnvalues.Imagineifanyoftheexpressionsontheright-handsideoftheequalssignintheaboveexampleswereactuallyfunctioncalls.
Destructuringisalsousefulforperformingmultipleassignmentsinasinglestatement.Forexample:
[foo,bar]=[bar,foo];//Swapfooandbarinplace
[previous,current]=[current,previous+current];
//CalculationstepforaFibonaccisequence
IntroducinggeneratorsES2016introducesgeneratorfunctionsandtheyieldkeyword.YoumayalreadybefamiliarwiththeyieldkeywordinC#.MethodsthatreturnIEnumerable/IEnumeratorcanincludetheyieldkeywordtoreturnoneelementatatime,suspendingexecutionofthemethoduntilthenextvalueisrequested.YoucandothesamewithgeneratorfunctionsinJavaScript.ThefollowingexampleisaJavaScriptimplementationofoneoftheexamplesfromtheMSDNdocumentationofC#'syield.Itprintsthefirsteightpowersof2(notetheasteriskafterthefunctionkeyword,whichdenotesthisasageneratorfunction):
'usestrict';
function*powers(number,exponent){
letresult=1;
for(leti=0;i<exponent;++i){
result=result*number;
yieldresult;
}
}
for(letiofpowers(2,8)){
console.log(i);
}
Notethatfor...ofloopsworkwithgenerators.Theaboveloopisequivalenttothefollowingcode:
letgenerator=powers(2,8);
letcurrent=generator.next();
while(!current.done){
console.log(current.value);
current=generator.next();
}
YoucanseethatgeneratorsareverysimilartotheIEnumeratorinterfaceinC#.Notethattheyareslightlymorepowerfulthanthisthough.Wecanalsopassavalueintoagenerator'snextmethodtoallowittobeusedwhenexecutioncontinuesinthegeneratorfunction.Thefollowingdummyexampleillustratesthis:
'usestrict';
function*generator(){
letreceived=yield1;
console.log(received);
return3;
}
letinstance=generator();
letfirst=instance.next();
console.log(first);
letlast=instance.next(2);
console.log(last);
Runningthepreviousexampleproducesthefollowingoutput:
>{value:1,done:false}
>2
>{value:3,done:true}
Thistwo-waycommunicationmakesgeneratorsmuchmorethanjustIEnumeratorforJavaScript.Theyareapowerfulcontrolflowmechanism,especiallywhencombinedwithpromises.Seehttps://www.promisejs.org/generators/foraderivationofC#-likeasync/awaitfunctionalityusinggeneratorsandpromises(withyieldtakingtheplaceofC#'sawaitkeyword).It'salsoworthnotingthatasyncfunctionsareplannedforafutureversionofECMAScript(probablyES2017)andwillworkinasimilarway.Inthemeantime,youcanachieveasimilarprogrammingmodelusingthePromise.coroutinemethodprovidedbythebluebirdlibrary,whichisbasedongenerators.Seehttp://bluebirdjs.com/docs/api/promise.coroutine.htmlfordetails.
IntroducingECMAScript2016Asmentionedearlierinthischapter,ECMAScript2016isasmallreleasewithonlyacoupleofnewfeatures.Theseareanincludesmethodforarraysandtheexponentationoperator**.
YoucanwritemyArray.includes(value)insteadofmyArray.indexOf(value)!==-1.Notethattheseexpressionsarenotquiteequivalent.YoucanuseincludestocheckforthevalueNaNwithinanarray,whichyoucan'tdowithindexOf.
TheexponentialoperatorallowsyoutorewriteMath.pow(coefficient,exponent)ascoefficient**exponent.
Youcanalsocombineitwithanassignment,asinmyVariable**=2.
GoingbeyondJavaScriptIfyouwanttotargetbrowsersorNode.js,JavaScriptistheonlylanguagenativelysupportedbytheseenvironments.ThisisdifferenttoVM-basedenvironmentslikethe.NETruntimeandtheJVM,whichsupportmultiplelanguages.
The.NETruntimesupportsC#,F#,VB.NET,andothers.TheJVMsupportsJava,Scala,Clojure,andothers.Theselanguagesworkbycompilingdowntoanassemblylanguagefortheenvironment'sVM.ThisistheCommonIntermediateLanguagein.NETorJavabytecodeinthecaseoftheJVM.
Thereisareasonwhyprogrammersdon'tallwriteCILorJavabytecodethough.Thesearelow-levelmachinelanguagesandmuchlesshuman-friendlythanC#,Java,andsoon.Ingeneral,higher-levellanguagescansupportbetterproductivity,aswellassafety(forexample,throughtypesystemsandmemorymanagement).
Thereisalsoareasonwhy.NETprogrammersdon'talwaysuseC#andJVMprogrammersdon'talwaysuseJava.Arangeoflanguagescanservedifferentusecasesbetter.Itcanalsojustbeamatterofpersonaltasteforthesemanticsofaparticularlanguage.
JavaScripthasbeencalledtheAssemblyLanguagefortheWeb(http://www.hanselman.com/blog/JavaScriptIsAssemblyLanguageForTheWebSematicMarkupIsDeadCleanVsMachinecodedHTML.aspxWhileJavaScriptisnotalow-levelormachinelanguage,itisacommonlanguageforitsplatform.LikeCILandJavabytecode,itcanserveasacompiletargetforotherlanguages.And,like.NETandtheJVM,thereisanappetiteamongstdevelopersforavarietyoflanguagesonthesameplatform.
Exploringcompile-to-JavaScriptlanguagesThereareseverallanguagesthatsupportwebandNode.jsdevelopmentbycompilingdowntoJavaScript.We'lllookatafewofthemoreprominentoftheselanguagesinthissection.
TypeScript
TheTypeScriptlanguageisdevelopedandsupportedbyMicrosoft.Itskeyaimistoincludefeaturesthataidlarge-scaleapplicationdevelopment.TypeScriptcanbecompileddowntoES2016,ES5,orevenES3.SoitworksinanymodernJavaScriptenvironment.
TypeScriptisbasedcloselyontheJavaScriptsyntax.ItisasupersetofJavaScript,soyoucanwriteordinaryJavaScriptandgraduallyuseTypeScriptfeaturesmoreasyoulearnit.TypeScriptalsotriestomatchthesyntaxofupcomingJavaScriptfeatureswherepossible.ThisallowsdeveloperstostartusingnewJavaScriptfeaturesearlier.
ThemostimportantTypeScriptfeaturesaidlarge-scaleapplicationdevelopment.TypeScripthashadclassesandmodulesforsometime,tohelpwithstructuringcode.Asthenamesuggests,TypeScriptalsoaddstypeannotationsandtypeinference.Italsoaddsnewwaysofdefiningandspecifyingtypes,includingenums,generictypes,andinterfaces.Thismakesforasaferlanguageasthecompilercancatchmoreerrors.ItalsoletsIDEsofferfeatureslikecodecompletion(namely,Intellisense)andbettersourcecodenavigation.
Finally,TypeScriptmakesitpossibletospecifytypedefinitionsforlibrarieswritteninplainJavaScript.Typedefinitionsformanythird-partylibrariescanbefoundathttps://github.com/DefinitelyTyped/DefinitelyTyped.Theseprovidetypecheckingandcodecompletionwhenworkingwithlibrarycodetoo.
Here'sanexampleofourflatMapfunctionfromthepreviouschapterwrittenwithtypeannotations:
functionflatMap<T,R>(
source:T[],
callback:(T)=>R[]):R[]{
returnArray.prototype.concat.apply([],
source.map(callback));
}
letresult=flatMap([1,2,3],(i:number)=>[i,i+0.5]);
console.log(result);//Prints[1,1.5,2,2.5,3,3.5]
ThesyntaxforgenericsmaybefamiliarfromC#.Typeannotationsfollowtheexpressionorparameter,separatedbyacolon.Wecouldspecifythegenerictypewhenwecallthefunctiontoo,butinthiscaseitcanbeinferred.Notethatourmethodhastwogenerictypes,asourcallbackcouldmaptoanarrayofadifferentelementtype.TheTypeScriptcompilerwillinferthetypeofresultasnumber[].Notethatthisinferenceactuallytakesafewsteps:
WespecifythatthecallbackparameterihasatypenumberTherefore,theexpressionsiandi+0.5alsobothhaveatypenumber
Therefore,theresulttypeofourcallbackisnumber[]Therefore,theargumentforthetypeparameterRmustbenumber
Ifwedidnotspecifythetypeofi,thenthecompilerwouldonlyinferthetypeofresultasany[],thatisanarray,butofanunspecifiedelementtype.
YoucanlearnmoreaboutTypeScriptathttp://www.typescriptlang.org/.
Tip
Ifyou'remorefamiliarwithJavathan.NET,andespeciallyifyou'refamiliarwiththeEclipseIDEinparticular,youmayalsobeinterestedinN4JS(http://numberfour.github.io/n4js/).ThislanguagehassimilargoalstoTypeScript,butisinspiredbyJavaandhasanIDEbasedonEclipse.
CoffeeScript
CoffeeScriptwasoneoftheearliestsuccessfulcompile-to-JavaScriptlanguages.CoffeeScriptstreamlinesthesyntaxofJavaScriptandaddsfeaturesforwritingmoreterseandexpressivecode.
CoffeeScriptisagoodexampleofwhentastemightinfluencelanguagechoice.DevelopersmayfindCoffeeScriptmorereadableand/oreasiertowrite.RubyorPythonprogrammersmaybeparticularlycomfortablewithCoffeeScript.They'llfinditssyntaxandmanyofitslanguagefeaturesfamiliar.
ManyfeaturesfromCoffeeScripthavesubsequentlyappearedinES2015,forexamplearrowfunctions,destructuring,andthesplat/spreadoperator.UnlikeTypeScript,CoffeeScriptdoesnotattempttomatchthesyntaxofJavaScript,neitherforcurrentnorupcomingfeatures.ItdoeshoweverofferseamlessinteroperabilitywithJavaScriptcode.
ComprehensionsareoneofCoffeeScript'smostexpressivefeaturesanddonotappearinES2015.YoumaybefamiliarwithcomprehensionsfromPython.TheyarealsoalittlelikeLINQinC#,inthattheyallowyoutoexpressoperationsonlistswithoutusingloops.Thefollowingexampleprintsthesquaresofevennumbers,firstinJavaScriptandthenasaone-linerinCoffeeScript.Assquares.js:
vari,n;
for(n=i=1;i<=10;n=++i){
if(n%2===0){
console.log(n*n);
}
}
Assquares.coffee:
console.logn*nfornin[1..10]whenn%2is0
Andbeyond...
TypeScriptandCoffeeScriptarespecificallydesignedtotargetJavaScript.TherearemanyotherprojectsinexistencethatallowmoregenerallanguagestocompileJavaScript.Notethatnotallsuchprojectsarematureorwell-maintained.LanguageswhoseownprojectteamsupportsandmaintainscompilationtoJavaScripttendtobeasaferchoice.BothDart(https://www.dartlang.org/)andClojure(http://clojure.org/)providefirst-classsupportforcompilingtoJavaScript.
IntroducingatrueassemblylanguageforthewebAsdiscussedabove,whileJavaScriptcanbeacommoncompiletargetforthewebandNode.js,itisnotatrueassemblylanguage.Itisahigh-levelhuman-readablelanguage,ratherthananoptimizedmachinelanguage.Thereareprojectstointroducejustsuchalanguageintothewebenvironmentthough.Thismeansdefininganassemblylanguageimplementedbyallbrowsers,includingChrome'sV8engineandthereforeNode.js.
Understandingasm.js
Thefirstattemptatsuchalanguageisasm.js(http://asmjs.org/),developedbyMozilla.ThisisastrictsubsetofJavaScript,whichmeansitcanrunonanybrowser.Butbrowsersthatsupportasm.jscanprecompileitandheavilyoptimizeitsexecution.Demandingapplicationssuchas3Dgamescanberecompiledtotargetasm.jsandrunseamlesslyin-browser.Thefirstenvironmentwithfullsupportforasm.jsisMozilla'sownFirefoxbrowser.ItwillalsobesupportedinMicrosoft'snewEdgebrowser.TheV8engineusedbyChrome(andNode.js)doesnotyetpre-compileasm.js,butV8doesmakesomeoptimizationstoallowasm.jstorunmuchfasterthanifinterpretedasplainJavaScript.
UnderstandingWebAssembly
WebAssembly(https://webassembly.github.io/)isanewstandardforatrueassemblylanguagefortheweb.Unlikeasm.jsitisnotasubsetofJavaScriptandwon'trunintoday'sbrowsers.ItdefinesanewassemblylanguagemorelikeCILorJavabytecode.ItisdevelopedbytheW3Cstandardsbody,withinputfromthemajorbrowservendors.ThereareearlyimplementationsofWebAssemblyinpreviewreleasesofMozillaFirefox,GoogleChrome,andMicrosoftEdge.
Asanapplicationdeveloper,youdonotneedtobeabletowriteWebAssemblyanymorethanyouneedtowriteCILorJavabytecode.Thesearealllow-levellanguagestoactascompilationtargets.Infuture,WebAssemblymayreplaceJavaScriptasthecommoncompiletargetfortheweb(andNode.js).Otherlanguages,includingJavaScriptitself,mayallcompiletoWebAssembly.
ThiswouldmeanthatJavaScriptwouldnolongerbetheonlynativelanguageforthewebandNode.js.ButJavaScriptwillalmostcertainlyremainthedefaultdevelopmentlanguagefortheseenvironments,justasC#andJavaarefortheirrespectiveenvironments.KnowledgeoftheexecutionmodelofNode.jswillstillberelevantinanylanguageandJavaScriptwillstillbethemostnaturalfitforthisexecutionmodel.KnowledgeofJavaScriptwillalsobeimportantforworkingwiththemanywell-establishedlibrariesbasedonit.
TherewouldbeotherbenefitstoJavaScriptfromWebAssembly.InteroperationbetweenJavaScriptandotherlanguageswillbecomeeasier.Therewillbemoreoptionsforimplementingperformance-criticalcode.NewversionsofJavaScriptwillbeabletorolloutmorequickly(asasingleJavaScripttoWebAssemblycompilercantargetallbrowserengines).
JavaScriptandASP.NETOntheserverside,wedon'tneedtowaitforWebAssemblytomatureinordertoworkwithNode.jsand.NETtogether.Thereisalreadysomeconvergencebetweenprogrammingonthesetwoplatformsandsupportforinteroperabilitybetweenthem.
Exploring.NETCoreThenextversionofNET,called.NETCore,makessomemajorchangestotheplatform.Someofthesechangesmightseemfamiliarifyou'vespentsometimeworkingwithNode.js.Thisisnotjustacoincidence.MicrosoftareincorporatinggoodideasthathaveworkedinNode.jsandelsewhereintotheirecosystem.
Definingprojectstructurein.NETCore
.NETCoreseparatestheprogrammingplatformfromtheIDE.MicrosoftstillrecommendsusingVisualStudio,buthavemadeitmucheasiertouseothereditors.Forexample,theOmniSharpproject(http://www.omnisharp.net/)supportsdevelopmentinothereditors,providingfeaturessuchasIntellisenseoutsideofVisualStudio.
Oneaspectofthischangeissimplifyingtheuseof.csprojfiles.Inpreviousversionsof.NET,theselargeXMLfileswerethecanonicaldescriptionofeachC#project.Theyincludedimportantthingslikecompilationoptions,targetplatforms,buildsteps,anddependencies.TheyweremainlygeneratedbyVisualStudio,difficulttoeditbyhand,andoftenparticularlyawkwardtomergeinsourcecontrol.TosatisfyVisualStudio,theyalsoneededtolisteverysinglesourcefileintheproject.
Manyofthesedrawbacksareaddressedin.NETCore.Newtoolsmakeitmucheasiertoedit.csprojfilesfromthecommandline.Aproject'ssourcesarejustthefilesunderitsparentfolder(notlistedin.csprojoranyothermetadatafile).DependenciesaredeclaredseparatelyinamorelightweightJSON-basedfile.
ManyoftheseimprovementsareinspiredbyprogrammingplatformslikeNode.js.Infact,earlyreleasecandidatesfor.NETCoreremovedtheneedfor.csprojfilesentirelyandintroducedproject.jsonfiles(justlikeinNode.js)fordefiningprojects.Although.NETCoreultimatelyuses.csprojfiles(forcontinuedcompatibilitywithMSBuild),itaimstokeepthoseaspectsofmorelightweightapproachesthataremostimportanttodevelopers.
Managingdependenciesin.NETCore
TheNuGetpackagemanagerhasbeenpartofthe.NETecosystemforseveralyears.NuGetbecomesevenmoreimportantin.NETCore.TheframeworkandruntimethemselvesaredistributedasNuGetpackages.DependenciesarespecifiedasNuGetpackagenames(andversions)ratherthanDLLpaths.NuGetpackagescanalsobeausefulunitofdeploymentforyourownprojects.
JustlikewithNode.js,youcancheckoutthesourcecodeofoneofyourdependenciestoalocalfolderandreferenceitthere.Thisallowsyoutotinkerwithopensourcelibrariesanddebugthemaspartofyourprogram.
BuildingwebapplicationsinASP.NETCore
ASP.NETCoreconsolidatesASP.NETMVCandWebAPIintoasingleframework.ItalsobringsOWINtotheforeasthestandardabstractionforimplementingwebapplications.
OWINsimplydefinesastandardforpassingrequestandresponseobjectsbetweenahostandanapplication.AlthoughOWINhasbeenaroundforawhileandhasitsownhistory,thisisasimilarabstractiontothehttp.createServermethodinNode.js.YoucanreadmoreaboutOWINathttps://docs.asp.net/en/latest/fundamentals/owin.html.
Relatedtothis,ASP.NETalsousesmiddlewareasthestandardbuildingblockforwebapplications.Again,althoughmiddlewarein.NEThasitsownhistory,theabstractionisverysimilartomiddlewareinExpress.Applicationssetupapipelineofmiddleware,witheachhavingaccesstotherequest,response,andthenexthandlerinthechain.Built-inmiddlewareisavailableforcross-cuttingconcernssuchasauthentication,sessions,androuting.Youcanreadmoreaboutmiddlewareathttps://docs.asp.net/en/latest/fundamentals/middleware.html
IntegrationwithJavaScriptVisualStudiohasprovidedgoodsupportforclient-sideJavaScriptdevelopmentforseveralyears.MicrosofthaveimprovedandupdatedthisinthelatestversionsofASP.NETandVisualStudio:forexample,byincludingbetterintegrationwithtaskrunnerssuchasGulpandGrunt.Youcanreadmoreaboutclient-sideJavaScriptsupportathttps://docs.asp.net/en/latest/client-side/index.html.
Server-sideJavaScriptintegrationwith.NET
TheEdge.jsproject(https://github.com/tjanczuk/edge)allowsNode.jsand.NETtorunwithinthesameprocess.Italsodefinesaverysimplewayformarshallingmethodcallsbetweenthetwo.Thisismuchfasterthanmarshallingcallsout-of-process(forexample,viaanHTTPcalltoaprocessonthelocalmachine).
Edge.jsallowsyoutotakethebestof.NETandNode.js.PerhapsyouwanttouseNode.jstoputawebinterfaceontopofyourexisting.NETbusinesslogic.Orperhapsyou'reusingNode.jsforrapiddevelopmentofmostofyourapplication,buthaveaparticularlyCPU-intensiveoperationthatwouldbeeasiertooptimizein.NET.
MakingcallsfromNode.jsto.NET(orviceversa)isverysimple.Forexample,ifwehavethefollowing.NETclass:
usingSystem;
usingSystem.Threading.Tasks;
namespaceDeepThought
{
publicclassUltimateQuestion
{
publicTask<Object>GetAnswer(objectinput){
varresult=new
{
description=
"AnswertoTheUltimateQuestionof"+input,
value=42
};
returnTask.FromResult<object>(result);
}
}
}
WecanuseitfromJavaScriptasfollows(afterrunningnpminstalledge):
'usestrict';
constedge=require('edge');
letgetAnswer=edge.func({
assemblyFile:'bin\\Debug\\DeepThought.dll',
typeName:'DeepThought.UltimateQuestion',
methodName:'GetAnswer'
});
getAnswer('Life,theUniverse,andEverything',(error,result)=>{
console.log(result);
});
CompilingourC#codeandrunningourJavaScriptfileresultsinthefollowingoutput:
>nodeindex.js
>{description:'AnswertoTheUltimateQuestionofLife,theUniverse,and
Everything',value:42}
YoucanfindagoodintroductiontoEdge.jsathttp://www.hanselman.com/blog/ItsJustASoftwareIssueEdgejsBringsNodeAndNETTogetherOnThreePlatforms.aspx
Finally,recallthattheOWINstandardandASP.NETmiddlewarearequitesimilartothecorrespondingconceptsinJavaScript.Edge.jsmakesiteasytoincludea.NETOWINapplicationasmiddlewareinaNode.jsExpressapplication.Seetheconnect-owinprojectathttps://github.com/bbaia/connect-owinfordetails.
SummaryInthischapter,wehaveseenhowNode.jsandJavaScript'snewreleasecyclesbringstabilitytotheplatform.WehaveintroducedsomeofthenewandupcomingfeaturesofJavaScript.WehaveexploredcurrentandfuturealternativelanguagesfortheJavaScriptenvironment.Wehaveseensomeofthecommonalitiesbetween.NETandNode.jsandhowtousethesetechnologiestogether.
Ihopethisbookhasallowedyoutogetup-and-runningwithNode.jsandgivenyouanappetitetolearnmore.TheresourcesinthischapterwillhelpyoutakethenextsteponyourjourneywithJavaScriptandNode.js.
IndexA
adapterpattern/UsingRedisasabackendafterEachhook
about/Resettingstatebetweentestsaggregationpipeline
about/UsingtheMongoDBshellAjax
used,forcommunication/CommunicatingviaAjaxalternativesessionstores
using/UsingalternativesessionstoresAMD
about/JavaScriptmodulesystemsAMDmodules
using,withRequireJS/UsingAMDmoduleswithRequireJSapp.jsfile/ExploringourExpressapplicationapplication
executing,locallywithHeroku/RunninganapplicationlocallywithHerokudeploying,withHeroku/DeployinganapplicationtoHeroku
applicationframeworkusing/UsinganapplicationframeworkExpress,using/GettingstartedwithExpress
arrayliteralnotationabout/FunctionalprogramminginJavaScript
asm.jsabout/Understandingasm.jsURL/Understandingasm.js
ASP.NETandJavaScript/JavaScriptandASP.NET.NETCore/Exploring.NETCoreintegration,withJavaScript/IntegrationwithJavaScriptserver-sideJavaScriptintegration/Server-sideJavaScriptintegrationwith.NET
assemblylanguageabout/Introducingatrueassemblylanguageforthewebasm.js/Understandingasm.jsWebAssembly/UnderstandingWebAssembly
assertionswriting,withChai/UsingChaiforassertions
asynchronouscodecallbackpattern,using/Usingthecallbackpatternforasynchronouscodewriting,promisesused/Writingcleanerasynchronouscodeusingpromisespromise-basedasynchronouscode,implementing/Implementingpromise-based
asynchronouscodeoperations,parallelizingwithpromises/Parallelisingoperationsusingpromises
asynchronousinterfacesconsuming/Consumingasynchronousinterfaces
AsynchronousModuleDefinition(AMD)/Writinguniversalmodulesasynchronousprogramming
about/Non-blockingasynchronousprogrammingpatterns
combining/Combiningasynchronousprogrammingpatternsasynchronoustests
writing,inMocha/TestinganExpressapplicationAtom
URL/Choosinganeditor
BBDD-styletests
writing,withMocha/WritingBDD-styletestswithMochastate,resetting/Resettingstatebetweentests
beforeEachhookabout/Resettingstatebetweentests
behavior-drivendevelopment(BDD)styleabout/WritingBDD-styletestswithMocha
bin/wwwfile/ExploringourExpressapplicationbinaryJSON(BSON)
about/IntroducingMongoDBbrowser
Node.jsmodules,using/UsingNode.jsmodulesinthebrowserBrowserify
URL/UsingNode.jsmodulesinthebrowser,ControllingBrowserify'soutputoutput,controlling/ControllingBrowserify'soutput
buildprocessautomating,withGulp/AutomatingthebuildprocesswithGulptests,executingwithGulp/RunningtestsusingGulp
Ccallbackfunction
about/Non-blockingcallbackpattern
using,asynchronouscode/Usingthecallbackpatternforasynchronouscodeexposing/Exposingthecallbackpatternasynchronousinterfaces,consuming/Consumingasynchronousinterfaces
Chaiused,forassertions/UsingChaiforassertionsURL/UsingChaiforassertions
chatroomimplementing,Socket.IOused/ImplementingachatroomwithSocket.IO
class-basedinheritanceabout/Class-basedinheritance
classesavoiding,inobject-orientedprogramming/Programmingwithoutclassesobjects,creatingwithnewkeyword/Creatingobjectswiththenewkeywordused,inobject-orientedprogramming/Programmingwithclassesclass-basedinheritance/Class-basedinheritance
client-sideJavaScriptreferencelink/IntegrationwithJavaScript
ClojureURL/Andbeyond...
codebaseorganizing/OrganizingyourcodebaseJavaScriptmodulesystems/JavaScriptmodulesystems
codecoveragestatisticsgathering/Gatheringcodecoveragestatistics
codestylechecking,withESLint/CheckingcodestylewithESLint
CoffeeScriptabout/CoffeeScript
collectionsabout/IntroducingMongoDB
CommonJSabout/JavaScriptmodulesystems
compile-to-JavaScriptlanguagesabout/Exploringcompile-to-JavaScriptlanguagesTypeScript/TypeScriptCoffeeScript/CoffeeScript
connect-owinprojectURL/Server-sideJavaScriptintegrationwith.NET
constkeyword
about/StrictmodeContinuousIntegration(CI)
about/SettingupanintegrationserverCookieChoices
URL/Decidingwhenthesessiongetssaved
DDart
URL/Andbeyond...databaseintegrationtests
executing,onTravisCI/RunningdatabaseintegrationtestsonTravisCIdataoperations
implementing/Implementingotherdataoperationsdata,listinginviews/Listingdatainviewsdeleterequest,issuingfromclient/IssuingadeleterequestfromtheclientExpressviews,splittingup/SplittingupExpressviewsusingpartials
DenialofService(DoS)attack/Runningautomatedclientsonthewebdependencies
managing,in.NETCore/Managingdependenciesin.NETCoredependencyinjection(DI)
inNode.js/DependencyinjectioninNode.jsdevelopmentdependency
about/WritingBDD-styletestswithMochadirectory-levelmodule
defining/Definingadirectory-levelmoduledocument-orientedDBMS
about/IntroducingMongoDBDocumentObjectModel(DOM)
about/WhatisNode.js?
EECMAScript
versioning/UnderstandingECMAScriptversioningECMAScript2015
exploring/ExploringECMAScript2015URL/ExploringECMAScript2015ES2015modules/UnderstandingES2015modulesgeneratorfunctions/Introducinggenerators
ECMAScript2016about/IntroducingECMAScript2016
Edge.jsURL/Server-sideJavaScriptintegrationwith.NET
Edge.jsprojectURL/Server-sideJavaScriptintegrationwith.NET
editorselecting/Choosinganeditor
encryptedenvironmentvariablesettingup/SettingencryptedTravisCIenvironmentvariablesRuby,installing/InstallingRubycreating/Creatinganencryptedenvironmentvariable
ES2015modulesabout/UnderstandingES2015modulesURL/UnderstandingES2015modulessyntaximprovements,using/UsingsyntaximprovementsfromES2015for...ofloop/Thefor...ofloopspreadoperator/Thespreadoperatorandrestparametersrestparameters/Thespreadoperatorandrestparametersassignment,destructuring/Destructuringassignment
ESLintcodestyle,checkingwith/CheckingcodestylewithESLintissues,fixingautomatically/AutomaticallyfixingissuesinESLintURL,forrules/AutomaticallyfixingissuesinESLintexecuting,fromGulp/RunningESLintfromGulp
event-drivenexecutionmodelabout/Event-driven
eventloopabout/Event-driven
executionmodel,Node.jsabout/UnderstandingtheNode.jsexecutionmodelnon-blocking/Non-blockingevent-driven/Event-drivensingle-threaded/Single-threaded
Express
using/GettingstartedwithExpressroutes/UnderstandingExpressroutesandviewsviews/UnderstandingExpressroutesandviewsnodemon,using/Usingnodemonforautomaticrestartsmodularapplications,creating/CreatingmodularapplicationswithExpressmiddleware/UnderstandingExpressmiddlewareMongoDB,usingwith/UsingMongoDBwithExpressSocket.IO,integrating/IntegratingSocket.IOwithExpress
Expressapplicationexploring/ExploringourExpressapplicationbootstrapping/BootstrappinganExpressapplicationtesting/TestinganExpressapplicationtests,simplifyingwithSuperAgent/SimplifyingtestsusingSuperAgent
Expressapplication,foldersnode_modules/ExploringourExpressapplicationpublic/ExploringourExpressapplicationroutes/ExploringourExpressapplicationviews/ExploringourExpressapplication
Expressmiddlewaremoduleimplementing/ImplementinganExpressmiddlewaremodule
Expresssessionsusing/UsingExpresssessionssessionsecret,specifying/Specifyingasessionsecretsession,saving/Decidingwhenthesessiongetssavedalternativesessionstores,using/Usingalternativesessionstoressessionmiddleware,using/Usingsessionmiddleware
Expressviewssplittingup,withpartials/SplittingupExpressviewsusingpartials
FFacebookapplication
URL/Addingotherloginprovidersfor...ofloop
about/Thefor...ofloopfull-stacktesting
withPhantomJS/Full-stacktestingwithPhantomJSfunctionalobject-orientedprogramming
about/Functionalobject-orientedprogrammingJavaScript/FunctionalprogramminginJavaScriptobject-orientedprogramming/Object-orientedprogramminginJavaScript
GGem
about/Creatinganencryptedenvironmentvariablegeneratorfunctions
about/Introducinggeneratorsreferencelink/Introducinggenerators
GitHubURL/SettingupapublicGitHubrepository,BuildingaprojectonTravisCI
governancemodelURL/AbriefhistoryofNode.js
Gulpbuildprocess,automating/AutomatingthebuildprocesswithGulptests,executing/RunningtestsusingGulpESLint,executingfrom/RunningESLintfromGulpintegrationtests,executing/RunningintegrationtestsfromGulp
HHangman
URL/Handlinguser-submitteddatauser-submitteddata,handling/Handlinguser-submitteddatacommunicating,viaAjax/CommunicatingviaAjaxdataoperations,implementing/Implementingotherdataoperations
hashesabout/StoringstructureddatainRedis
Herokuabout/WorkingwithHerokuURL/WorkingwithHerokuaccount,settingup/SettingupaHerokuaccountandtoolingapplication,executing/RunninganapplicationlocallywithHerokuapplication,deploying/DeployinganapplicationtoHerokuMongoDB,settingup/SettingupMongoDBRedis,settingup/SettingupRedis
Herokuconfigworkingwith/WorkingwithHerokulogs,config,andservices
Herokulogsworkingwith/WorkingwithHerokulogs,config,andservices
Herokuservicesworkingwith/WorkingwithHerokulogs,config,andservices
herokutoolbeltURL/SettingupaHerokuaccountandtooling
higher-orderfunctionsabout/FunctionalprogramminginJavaScript
IImmediately-InvokedFunctionExpression(IIFE)/Supportingthebrowserenvironmentintegrationserver
about/Settingupanintegrationserversettingup/SettingupanintegrationserverpublicGitHubrepository,settingup/SettingupapublicGitHubrepositoryproject,buildingonTravisCI/BuildingaprojectonTravisCI
integrationtestsexecuting,fromGulp/RunningintegrationtestsfromGulp
io.jsabout/AbriefhistoryofNode.js
IsomorphicJavaScriptURL/IsomorphicJavaScript
JJasmine
about/WritingBDD-styletestswithMochaJavaScript
about/WhyJavaScript?,JavaScriptcanvas/Aclearcanvasfunctions/Functionalnaturefuture/Abrightfuturefunctionalprogramming/FunctionalprogramminginJavaScriptscopes/UnderstandingscopesinJavaScriptobject-orientedprogramming/Object-orientedprogramminginJavaScriptexploring/GoingbeyondJavaScriptreferencelink/GoingbeyondJavaScriptcompile-to-JavaScriptlanguages/Exploringcompile-to-JavaScriptlanguagesassemblylanguage/IntroducingatrueassemblylanguageforthewebandASP.NET/JavaScriptandASP.NETASP.NETintegration/IntegrationwithJavaScript
JavaScriptmodulesystemsabout/JavaScriptmodulesystems
JavaScriptprimitivetypesabout/JavaScriptprimitivetypes
JavaScripttypesabout/IntroducingJavaScripttypesprimitivetypes/JavaScriptprimitivetypes
LLanguage-IntegratedQuery(LINQ)
about/FunctionalprogramminginJavaScriptletkeyword
about/Strictmodelists
about/StoringstructureddatainRedisloginproviders
adding/Addingotherloginproviderslong-polling/Understandingoptionsforreal-timecommunicationlong-termsupport(LTS)
about/IntroducingtheNode.jsLTSscheduleLTSschedule
about/IntroducingtheNode.jsLTSscheduleURL/IntroducingtheNode.jsLTSschedule
MMap-Reduce
about/UsingtheMongoDBshellURL/UsingtheMongoDBshell
middlewareabout/BuildingwebapplicationsinASP.NETCoreURL/BuildingwebapplicationsinASP.NETCore
middleware,Expressabout/UnderstandingExpressmiddlewareerrorhandling,implementing/Implementingerrorhandlingusing/UsingExpressmiddleware
MochaBDD-styletests,writing/WritingBDD-styletestswithMochaabout/WritingBDD-styletestswithMochaasynchronoustests,writing/TestinganExpressapplication
Mockgooseabout/Providingdependencies
mocksabout/CreatingtestdoublesusingSinon.JS
modelabout/PersistingobjectswithMongoose
modularapplicationscreating,withExpress/CreatingmodularapplicationswithExpress
modulecountsreferencelink/IntroducingtheNode.jsecosystem
modulesabout/Organizingyourcodebasecreating/CreatingmodulesinNode.jsdeclaring,withname/Declaringamodulewithanameanditsownscopedeclaring,withscope/Declaringamodulewithanameanditsownscopefunctionality,defining/Definingfunctionalityprovidedbythemoduleimporting,intoanotherscript/Importingamoduleintoanotherscriptdirectory-levelmodule,defining/Definingadirectory-levelmoduleExpressmiddlewaremodule,implementing/ImplementinganExpressmiddlewaremodule
ModulusURL/Furtherresources
MongoDBabout/IntroducingMongoDBadvantages/WhychooseMongoDB?objectmodelling/ObjectmodelingJavaScript/JavaScriptscalability/Scalability
URL/GettingstartedwithMongoDBURL,forinstallation/GettingstartedwithMongoDBdirectory,creating/GettingstartedwithMongoDBshell,using/UsingtheMongoDBshellURL,fordocumentation/UsingtheMongoDBshellusing,withExpress/UsingMongoDBwithExpressobjects,persistingwithMongoose/PersistingobjectswithMongoosepersistencecode,isolating/Isolatingpersistencecodedependencyinjection(DI),inNode.js/DependencyinjectioninNode.jsdependencies,providing/Providingdependenciesdatabaseintegrationtests,executing/RunningdatabaseintegrationtestsonTravisCIsettingup/SettingupMongoDB
Mongooseabout/UsingMongoDBwithExpressobjects,persisting/PersistingobjectswithMongoose
MustacheURL/UnderstandingExpressroutesandviews
N.NETCore
about/Exploring.NETCoreprojectstructure,defining/Definingprojectstructurein.NETCoredependencies,managing/Managingdependenciesin.NETCorewebapplications,building/BuildingwebapplicationsinASP.NETCore
N4JSURL/TypeScript
namespacesused,fororganizingSocket.IOapplications/OrganizingSocket.IOapplicationsusingnamespaces
newkeywordused,forcreatingobjects/Creatingobjectswiththenewkeyword
Node.jsabout/WhatisNode.js?ecosystem/IntroducingtheNode.jsecosystemusage/WhentouseNode.jswebapplications,writing/Writingwebapplicationsusecases/Identifyingotherusecasesneedfor/Whynow?URL/InstallingandrunningNode.jsinstalling/InstallingandrunningNode.jsrunning/InstallingandrunningNode.jsdependencyinjection(DI)/DependencyinjectioninNode.jsRedis,using/UsingRedisfromNode.jsandRequireJS,comparing/ComparingNode.jsandRequireJSversioning/UnderstandingNode.jsversioninghistory/AbriefhistoryofNode.jsLTSschedule/IntroducingtheNode.jsLTSschedule
Node.jsmodulesusing,inbrowser/UsingNode.jsmodulesinthebrowserBrowserifyoutput,controlling/ControllingBrowserify'soutput
nodemonusing/Usingnodemonforautomaticrestarts
non-blockingexecutionmodelabout/Non-blocking
npmabout/IntroducingtheNode.jsecosystempackage,publishing/Publishingapackagetonpm
npmcommandlinetoolabout/IntroducingtheNode.jsecosystem
npmpackageswriting/Writingnpmpackages
defining/Definingannpmpackagestandalonetool,releasing/Releasingastandalonetooltonpm
npmregistryabout/IntroducingtheNode.jsecosystem
NuGetpackagemanagerabout/Managingdependenciesin.NETCore
Nulltypeabout/JavaScriptprimitivetypes
numberabout/JavaScriptprimitivetypes
Oobject
about/Object-orientedprogramminginJavaScriptobject-oriented(OO)
about/WhyJavaScript?object-orientedprogramming
inJavaScript/Object-orientedprogramminginJavaScriptwithoutclasses/Programmingwithoutclasseswithclasses/Programmingwithclasses
Object-RelationalMapper(ORM)about/Objectmodeling
objectmodellingabout/Objectmodeling
objectscreating,withnewkeyword/Creatingobjectswiththenewkeyword
OmniSharpprojectURL/Definingprojectstructurein.NETCore
OpenIDConnectURL/Addingotherloginproviders
OWINabout/BuildingwebapplicationsinASP.NETCoreURL/BuildingwebapplicationsinASP.NETCore
Ppackage
publishing,tonpm/Publishingapackagetonpmautomatedclients,runningonweb/Runningautomatedclientsonthewebstandalonetool,releasingtonpm/Releasingastandalonetooltonpm
package.jsonfile/ExploringourExpressapplicationPassport
about/IntroducingPassportauthenticationstrategy,selecting/Choosinganauthenticationstrategythird-partyauthentication/Understandingthird-partyauthenticationconfiguring/ConfiguringPassportconfiguring,withpersistence/ConfiguringPassportwithpersistenceintegrationtesting/IntegrationtestingwithPassport
PhantomJSfull-stacktesting/Full-stacktestingwithPhantomJSabout/Full-stacktestingwithPhantomJS
pipelinestagesabout/UsingtheMongoDBshell
Platform-as-a-Service(PaaS)/FurtherresourcesProcfile
about/RunninganapplicationlocallywithHerokupromise-basedasynchronouscode
implementing/Implementingpromise-basedasynchronouscodeconsuming/Consumingthepromisepattern
Promise.coroutinemethodURL/Introducinggenerators
promisesused,forwritingasynchronouscode/Writingcleanerasynchronouscodeusingpromisesabout/Writingcleanerasynchronouscodeusingpromisespendingstates/Writingcleanerasynchronouscodeusingpromisesfulfilledstates/Writingcleanerasynchronouscodeusingpromisesrejectedstates/Writingcleanerasynchronouscodeusingpromisesused,forparallelizingoperations/Parallelisingoperationsusingpromises
Promises/A+URL/Combiningasynchronousprogrammingpatterns
prototypalinheritanceabout/Programmingwithoutclasses
prototypeabout/Programmingwithoutclasses
publicGitHubrepositorysettingup/SettingupapublicGitHubrepository
RRead-Eval-PrintLoop(REPL)
about/WhatisNode.js?real-timecommunication
options/Understandingoptionsforreal-timecommunicationreal-timeNode.jsapplications
scaling/Scalingreal-timeNode.jsapplicationsRedis,usingasbackend/UsingRedisasabackend
Redisabout/IntroducingRedisadvantages/WhyuseRedis?installing/InstallingRedisURL/InstallingRedisused,askey-valuestore/UsingRedisasakey-valuestorestructureddata,storing/StoringstructureddatainRedislists/StoringstructureddatainRedishashes/StoringstructureddatainRedissets/StoringstructureddatainRedissortedsets/StoringstructureddatainRedisuserrankingsystem,building/BuildingauserrankingsystemwithRedisusing,fromNode.js/UsingRedisfromNode.jssettingup/SettingupRedisuserdata,persisting/PersistinguserdatawithRedis
redis-jsused,fortesting/Testingwithredis-js
relationalpropertyabout/IntroducingMongoDB
RequireJSandNode.js,comparing/ComparingNode.jsandRequireJS
restparametersabout/Thespreadoperatorandrestparameters
robots.txtfileURL/Runningautomatedclientsontheweb
roomsused,forpartitioningSocket.IOclients/PartitioningSocket.IOclientsusingrooms
routes,Expressabout/UnderstandingExpressroutesandviews
Rubyinstalling/InstallingRubyURL/InstallingRuby
RubyInstallerURL/InstallingRuby
Sschema
about/PersistingobjectswithMongoosescopes,JavaScript
about/UnderstandingscopesinJavaScriptglobal/UnderstandingscopesinJavaScriptfunctional/UnderstandingscopesinJavaScriptstrictmode/Strictmode
securityabout/Anoteonsecurityreferences/Anoteonsecurity
SemanticVersioning2.0.0URL/AbriefhistoryofNode.js
server-sideJavaScriptintegrationabout/Server-sideJavaScriptintegrationwith.NET
sessionmiddlewareusing/Usingsessionmiddleware
setsabout/StoringstructureddatainRedis
single-threadedexecutionmodelabout/Single-threaded
Sinon.JSused,forcreatingtestdoubles/CreatingtestdoublesusingSinon.JSURL/CreatingtestdoublesusingSinon.JS
socialloginimplementing/ImplementingsocialloginTwitterapplication,settingup/SettingupaTwitterapplicationPassport,configuring/ConfiguringPassportuserdata,persistingwithRedis/PersistinguserdatawithRedisPassport,configuringwithpersistence/ConfiguringPassportwithpersistencefunctionality,hidingfromunauthenticatedusers/Hidingfunctionalityfromunauthenticatedusersintegrationtesting,withPassport/IntegrationtestingwithPassport
Socket.IOabout/IntroducingSocket.IOchatroom,implementing/ImplementingachatroomwithSocket.IOintegrating,withExpress/IntegratingSocket.IOwithExpressmessages,directing/DirectingSocket.IOmessagesapplications,testing/TestingSocket.IOapplications
Socket.IOapplications,organizingabout/OrganizingSocket.IOapplicationsreal-timeupdates,exposingtomodel/Exposingreal-timeupdatestothemodelnamespacesused/OrganizingSocket.IOapplicationsusingnamespaces
Socket.IOclientspartitioning,roomsused/PartitioningSocket.IOclientsusingroomssortedsets
about/StoringstructureddatainRedisspies
about/CreatingtestdoublesusingSinon.JSspreadoperator
about/Thespreadoperatorandrestparametersspy
about/Creatingtestdoublesstrictmock
about/CreatingtestdoublesusingSinon.JSstrictmode
about/Strictmodestrings
about/JavaScriptprimitivetypesStrings
about/UsingRedisasakey-valuestorestubs
about/CreatingtestdoublesusingSinon.JSSuperAgent
tests,simplifyingwith/SimplifyingtestsusingSuperAgentURL/SimplifyingtestsusingSuperAgent
SuperTestURL/SimplifyingtestsusingSuperAgent
Ttableofcontents(ToC)/Writingnpmpackagestemplatingengines
about/GettingstartedwithExpresstestdoubles
creating/Creatingtestdoublesabout/Creatingtestdoublescreating,Sinon.JSused/CreatingtestdoublesusingSinon.JS
testswriting/WritingasimpletestinNode.jscodebase,structuring/Structuringthecodebasefortestssimplifying,withSuperAgent/SimplifyingtestsusingSuperAgent
TravisCIURL/Settingupanintegrationserver,BuildingaprojectonTravisCIusing/Settingupanintegrationserverproject,building/BuildingaprojectonTravisCIdatabaseintegrationtests,executing/RunningdatabaseintegrationtestsonTravisCIused,fordeploying/DeployingfromTravisCIencryptedenvironmentvariable,settingup/SettingencryptedTravisCIenvironmentvariables
Twelve-FactorAppURL/Furtherresources
TwitterURL/SettingupaTwitterapplication
Twitterapplicationsettingup/SettingupaTwitterapplication
typedefinitionsreferencelink/TypeScript
TypeScriptabout/TypeScriptURL/TypeScript
UUndefinedtype
about/JavaScriptprimitivetypesuniversalmodules
writing/WritinguniversalmodulesNode.jsandRequireJS,comparing/ComparingNode.jsandRequireJSbrowserenvironment,supporting/SupportingthebrowserenvironmentAMDmodules,usingwithRequireJS/UsingAMDmoduleswithRequireJSisomorphicJavaScript/IsomorphicJavaScript
user-submitteddatahandling/Handlinguser-submitteddata
userrankingsystembuilding,withRedis/BuildingauserrankingsystemwithRedisuserrankings,implementing/ImplementinguserrankingswithRedisusersservice,using/Makinguseoftheusersservice
usersallowing,tologout/Allowinguserstologout
Vvariablehoisting
about/UnderstandingscopesinJavaScriptverifycallback
about/ConfiguringPassportviews,Express
about/UnderstandingExpressroutesandviewsVisualStudioCode
URL/Choosinganeditor