UpgradetoMicrosoftEdgetotakeadvantageofthelatestfeatures,securityupdates,andtechnicalsupport.
AddyOsmani|August4,2011
InPart1,I'llbecoveringacompleterun-downofBackbone0.5.2'smodels,views,collectionsandroutersbutalsotakingyouthroughoptionsforcorrectlynamespacingyourBackboneapplication.I'llalsogiveyousometipsincludingwhatscaffoldingtoolthatcansavetimesettingupyourinitialapplication,theidealnumberofrouterstouseandmore.
We'llthenbuildatestablewireframeofourapplicationinjQueryMobilebeforewecompletebuildingitinPart2.
DevelopersthathaveattemptedtoworkwithBackbone.jsandjQueryMobileinthepastwillbeawarethatbothsolutionshavetheirownapproachestohandingrouting.
Unfortunately,thereareoftenanumberofhacksandworkaroundsrequiredtogetthemfunctioninginunison.I'veexperienceddevelopersgoingsofarastoeditthejQueryMobilesourcetoachievethiswhichisactuallyunnecessary.InPart2,you'llfindouthowtousesome(currentlyundocumented)jQueryMobilefeaturestosolvethisissuewithoutresortingtosuchlengths,soyoucanspendmoretimeworkingonyouractualapplicationlogic.
I'mhopefultherewillbesomethinginthetutorialtobenefitdevelopersofallskill-levels,soonthatnote,let'sgetstarted!
Backboneismaintainedbyanumberofcontributors,mostnotably:JeremyAshkenas,creatorofCoffeeScript,DoccoandUnderscore.js.AsJeremyisabelieverindetaileddocumentation,there'salevelofcomfortinknowingyou'reunlikelytorunintoissueswhichareeithernotexplainedintheofficialdocsorwhichcan'tbenaileddownwithsomeassistancefromthe#documentcloudIRCchannel.Istronglyrecommendusingthelatterifyoufindyourselfgettingstuck.
Backbone'smainbenefits,regardlessofyourtargetplatformordevice,includehelping:
Inways,therealquestioniswhyyoushouldconsiderapplyingtheMVC-patterntoyourJavaScriptprojectsandtheonewordanswerissimplystructure.
IfoptingtousejQuery,zeptooranotherqSA-basedselectionlibrarytoproduceanon-trivialapplicationitcanbecomeveryeasytoproduceanunwieldyamountofcode;thatis-unlessyouhaveaplanforhowyou'regoingtostructureandorganizeyourapplication.Separatingconcernsintomodels,viewsandcontrollers(orrouters)isonewayofsolvingthis.
RememberthatifyouhaveexperiencewithstructuringapplicationsusingtheMVVM(Model-ViewViewModel)pattern,modulesorotherwise,thesearealsoequallyasvalidbutdorequireyoutoknowwhatyou'redoing.Formostsingle-pageapplications,IfindthattheMVCpatternworkswellsoBackboneisaperfectfitforourneeds.
Inthissection,you'lllearntheessentialsaboutBackbone'smodels,views,collectionsandrouters.Whilstthisisn'tmeantasareplacementfortheofficialdocumentation,itwillhelpyouunderstandmanyofthecoreconceptsbehindBackbonebeforewebuildmobileapplicationswithit.Iwillalsobecoveringsometipsoneffectivenamespacing.
Backbonemodelscontaininteractivedataforanapplicationaswellasthelogicaroundthisdata.Forexample,wecanuseamodeltorepresenttheconceptofaphotoobjectincludingitsattributesliketags,titlesandalocation.
Modelsarequitestraight-forwardtocreateandcanbeconstructedbyextendingBackbone.Modelasfollows:
Photo=Backbone.Model.extend({defaults:{src:'placeholder.jpg',title:'animageplaceholder,coordinates:[0,0]},initialize:function(){this.bind("change:src",function(){varsrc=this.get("src");console.log('Imagesourceupdatedto'+src)});},changeSrc:function(source){this.set({src:source});}});varsomePhoto=newPhoto({src:"test.jpg",title:"testing"});InitializationTheinitialize()methodiscalledwhencreatinganewinstanceofamodel.It'sconsideredoptional,howeverwe'llbereviewingsomereasonsitmightcomeinusefulveryshortly.
Photo=newBackbone.Model.extend({initialize:function(){console.log('thismodelhasbeeninitialized');}});/*Wecanthencreateourowninstanceofaphotoasfollows:*/varmyPhoto=newPhoto;Getters&SettersModel.get()
Model.get()provideseasyaccesstoamodel'sattributes.Attributesbelowwhicharepassedthroughtothemodeloninstantiationaretheninstantlyavailableforretrieval.
varmyPhoto=newPhoto({title:"Myawesomephoto",src:"boston.jpg",location:"Boston",tags:['thebiggame','vacation']}),title=myPhoto.get("title"),//myawesomephotolocation=myPhoto.get("location"),//Bostontags=myPhoto.get("tags"),//['thebiggame','vacation']photoSrc=myPhoto.get("src");//boston.jpgAlternatively,ifyouwishtodirectlyaccessalloftheattributesinanmodel'sinstancedirectly,youcanachievethisasfollows:
varmyAttributes=myPhoto.attributes;console.log(myAttributes);Note:ItisbestpracticetouseModel.set()ordirectinstantiationtosetthevaluesofamodel'sattributes.AccessingModel.attributesdirectlyisfineforreadingorcloningdata,butideallyshouldn'tbeusedtoforattributemanipulation.
Finally,ifyouwouldliketocopyamodel'sattributesforJSONstringification(e.g.forserializationpriortobeingpassedtoaview),thiscanbeachievedusingModel.toJSON():
varmyAttributes=myPhoto.toJSON();console.log(myAttributes);/*thisreturns{title:"Myawesomephoto",src:"boston.jpg",location:"Boston",tags:['thebiggame','vacation']}*/Model.set()
Model.set()allowsustopassparametersintoaninstanceofourmodel.Attributescaneitherbesetduringinitializationorlateron.
Photo=newBackbone.Model.extend({initialize:function(){console.log('thismodelhasbeeninitialized');}});/*Settingthevalueofattributesviainstantiation*/varmyPhoto=newPhoto({title:'Myawesomephoto',location:'Boston'});varmyPhoto2=newPhoto();/*SettingthevalueofattributesthroughModel.set()*/myPhoto2.set({title:'VacationinFlorida',location:'Florida'});DefaultvaluesTherearetimeswhenyouwantyourmodeltohaveasetofdefaultvalues(e.g.inscenariowhereacompletesetofdataisn'tprovidedbytheuser).Thiscanbesetusingapropertycalled'defaults'inyourmodel.
Photo=newBackbone.Model.extend({defaults:{title:'Anotherphoto!',tags:['untagged'],location:'home',src:'placeholder.jpg'},initialize:function(){}});varmyPhoto=newPhoto({location:"Boston",tags:['thebiggame','vacation']}),title=myPhoto.get("title"),//Anotherphoto!location=myPhoto.get("location"),//Bostontags=myPhoto.get("tags"),//['thebiggame','vacation']photoSrc=myPhoto.get("src");//placeholder.jpgListeningforchangestoyourmodelAnyandalloftheattributesinaBackbonemodelcanhavelistenersboundtothemwhichdetectwhentheirvalueschange.Thiscanbeeasilyaddedtotheinitialize()functionasfollows:
this.bind('change',function(){console.log('valuesforthismodelhavechanged');});Inthefollowingexample,wecanalsologamessagewheneveraspecificattribute(thetitleofourPhotomodel)isaltered.
Photo=newBackbone.Model.extend({defaults:{title:'Anotherphoto!',tags:['untagged'],location:'home',src:'placeholder.jpg'},initialize:function(){console.log('thismodelhasbeeninitialized');this.bind("change:title",function(){vartitle=this.get("title");console.log("Mytitlehasbeenchangedto.."+title);});},setTitle:function(newTitle){this.set({title:newTitle});}});varmyPhoto=newPhoto({title"Fishingatthelake",src:"fishing.jpg")});myPhoto.setTitle('Fishingatsea');//logsmytitlehasbeenchangedto..FishingatseaValidationBackbonesupportsmodelvalidationthroughModel.validate(),whichallowscheckingtheattributevaluesforamodelpriortothembeingset.
Itsupportsincludingascomplexortersevalidationrulesagainstattributesandisquitestraight-forwardtouse.Iftheattributesprovidedarevalid,nothingshouldbereturnedfrom.validate(),howeveriftheyareinvalidacustomerrorcanbereturnedinstead.
Abasicexampleforvalidationcanbeseenbelow:
Photo=newBackbone.Model.extend({validate:function(attribs){if(attribs.src=="placeholder.jpg"){return"Remembertosetasourceforyourimage!";}},initialize:function(){console.log('thismodelhasbeeninitialized');this.bind("error",function(model,error){console.log(error);});}});varmyPhoto=newPhoto();myPhoto.set({title:"Onthebeach"});CollectionsCollectionsarebasicallysetsofmodelsandcanbeeasilycreatedbyextendingBackbone.Collection.
Normally,whencreatingacollectionyou'llalsowanttopassthroughapropertyspecifyingthemodelthatyourcollectionwillcontainaswellasanyinstancepropertiesrequired.
Inthefollowingexample,we'recreatingaPhotoCollectioncontainingthePhotomodelswepreviouslydefined.
PhotoCollection=Backbone.Collection.extend({model:Photo});GettersandSettersThereareafewdifferentoptionsforretrievingamodelfromacollection.Themoststraight-forwardisusingCollection.get()whichacceptsasingleidasfollows:
varskiingEpicness=PhotoCollection.get(2);Sometimesyoumayalsowantgetamodelbasedonsomethingcalledtheclientid.Thisisanidthatisinternallyassignedautomaticallywhencreatingmodelsthathavenotyetbeensaved,shouldyouneedtoreferencethem.Youcanfindoutwhatamodel'sclientidisbyaccessingits.cidproperty.
varmySkiingCrash=PhotoCollection.getByCid(456);BackboneCollectionsdon'thavesettersassuch,butdosupportaddingnewmodelsvia.add()andremovingmodelsvia.remove().
vara=newBackbone.Model({title:'myvacation'}),b=newBackbone.Model({title:'myholiday'});varphotoCollection=newBackbone.Collection([a,b]);photoCollection.remove([a,b]);ListeningforeventsAscollectionsrepresentagroupofitems,we'realsoabletolistenforaddandremoveeventsforwhennewmodelsareaddedorremovedfromthecollection.Here'sanexample:
PhotoCollection=newBackbone.Collection;PhotoCollection.bind("add",function(photo){console.log("Iliked"+photo.get("title")+'itsthisone,right'+photo.src);});PhotoCollection.add([{title:"MytriptoBali",src:"bali-trip.jpg"},{title:"Theflighthome",src:"long-flight-oofta.jpg"},{title:"Uploadingpix",src:"too-many-pics.jpg"}]);Inaddition,we'reabletobinda'change'eventtolistenforchangestomodelsinthecollection.
PhotoCollection.bind("change:title",function(){console.log('therehavebeenupdatesmadetothiscollectionstitles');});FetchingmodelsfromtheserverCollections.fetch()providesyouwithasimplewaytofetchadefaultsetofmodelsfromtheserverintheformofaJSONarray.Whenthisdatareturns,thecurrentcollectionwillrefresh.
varPhotoCollection=newBackbone.Collection;PhotoCollection.url='/photos';PhotoCollection.fetch();Underthecovers,Backbone.syncisthefunctioncalledeverytimeBackbonetriestoread(orsave)modelstotheserver.ItusesjQueryorZepto'sajaximplementationstomaketheseRESTfulrequests,howeverthiscanbeoverriddenasperyourneeds.
Intheabovefetchexampleifwewishtologaneventwhen.sync()getscalled,wecansimplyachievethisasfollows:
Backbone.sync=function(method,model){console.log("I'vebeenpassed"+method+"with"+JSON.stringify(model));};Resetting/RefreshingCollectionsRatherthanaddingorremovingmodelsindividually,youoccasionallywishtoupdateanentirecollectionatonce.Collection.reset()allowsustoreplaceanentirecollectionwithnewmodelsasfollows:
PhotoCollection.reset([{title:"MytriptoScotland",src:"scotland-trip.jpg"},{title:"TheflightfromScotland",src:"long-flight.jpg"},{title:"Latestsnapoflock-ness",src:"lockness.jpg"}]);UnderscoreutilityfunctionsAsBackbonerequiresUnderscoreasaharddependency,we'reabletousemanyoftheutilitiesithastooffertoaidwithourapplicationdevelopment.Here'sanexampleofhowUnderscore'ssortBy()methodcanbeusedtosortacollectionofphotosbasedonaparticularattribute.
Note:ArouterwillusuallyhaveatleastoneURLroutedefinedaswellasafunctionthatmapswhathappenswhenyoureachthatparticularroute.Thistypeofkey/valuepairmayresemble:
"/route":"mappedFunction"
LetusnowdefineourfirstcontrollerbyextendingBackbone.Router.Forthepurposesofthisguide,we'regoingtocontinuepretendingwe'recreatingaphotogalleryapplicationthatrequiresaGalleryController.
Notetheinlinecommentsinthecodeexamplebelowastheycontinuetherestofthelessononrouters.
Next,weneedtoinitializeBackbone.historyasithandleshashchangeeventsinourapplication.Thiswillautomaticallyhandleroutesthathavebeendefinedandtriggercallbackswhenthey'vebeenaccessed.
TheBackbone.history.start()methodwillsimplytellBackbonethatit'sOKtobeginmonitoringallhashchangeeventsasfollows:
Backbone.history.start();Controller.saveLocation()Asanaside,ifyouwouldliketosaveapplicationstatetotheURLataparticularpointyoucanusethe.saveLocation()methodtoachievethis.ItsimplyupdatesyourURLfragmentwithouttheneedtotriggerthehashchangeevent.
/*Letsimaginewewouldlikeaspecificfragmentforwhenauserzoomsintoaphoto*/zoomPhoto:function(factor){this.zoom(factor);//imaginethiszoomsintotheimagethis.saveLocation("zoom/"+factor);//updatesthefragmentforus}ViewsViewsinBackbonedon'tcontainthemarkupforyourapplication,butratheraretheretosupportmodelsbydefininghowtheyshouldbevisuallyrepresentedtotheuser.ThisisusuallyachievedusingJavaScripttemplating(e.g.Mustache,jQuerytmpletc).Whenamodelupdates,ratherthantheentirepageneedingtoberefreshed,wecansimplybindaview'srender()functiontoamodel'schange()event,allowingtheviewtoalwaysbeuptodate.
Similartotheprevioussections,creatinganewviewisrelativelystraight-forward.WesimplyextendBackbone.View.Here'sanexampleofaonepossibleimplementationofthis,whichI'llexplainshortly:
varPhotoSearch=Backbone.View.extend({el:$('#results'),render:function(event){varcompiled_template=_.template($("#results-template").html());this.el.html(compiled_template(this.model.toJSON()));returnthis;//recommendedasthisenablescallstobechained.},events:{"submit#searchForm':"search","click.reset':"reset","click.advanced":'switchContext"},search:function(event){//executedwhenaform'#searchForm'hasbeensubmitted},reset:function(event){//executedwhenanelementwithclass"go"hasbeenclicked.},//etc});Whatis'el'elisbasicallyareferencetoaDOMelementandallviewsmusthaveone,howeverBackboneallowsyoutospecifythisinfourdifferentways.Youcaneitherdirectlyuseanid,atagName,classNameorifyoudon'tstateanythingelwillsimplydefaulttoaplaindivelementwithoutanyidorclass.Herearesomequickexamplesofhowthesemaybeused:
el:$('#results')//selectbasedonIDorjQueryselector.tagName:'li'//selectbasedonaspecifictag.HereelitselfwillbederivedfromthetagNameclassName:'items'//similartotheabove,thiswillalsoresultinelbeingderivedfromitel:''//defaultstoadivwithoutanid,nameorclass.Understandingrender()render()isafunctionthatshouldalwaysbeoverriddentodefinehowyouwouldlikeatemplatetoberendered.BackboneallowsyoutouseanyJavaScripttemplatingsolutionofyourchoiceforthisbutforthepurposesofthisexample,we'lloptforunderscore'smicro-templating.
The_.templatemethodinunderscorecompilesJavaScripttemplatesintofunctionswhichcanbeevaluatedforrendering.Here,I'mpassingthemarkupfromatemplatewithid'results-template'tobecompiled.Next,Isetthehtmlforel(ourDOMelementforthisview)theoutputofprocessingaJSONversionofthemodelassociatedwiththeviewthroughthecompiledtemplate.
Presto!Thispopulatesthetemplate,givingyouadata-completesetofmarkupinjustafewshortlinesofcode.
TheBackboneeventsattributeallowsustoattacheventlistenerstoeithercustomselectorsorelifnoselectorisprovided.Aneventtakestheform{"eventNameselector":"callbackFunction"}andanumberofevent-typesaresupported,including'click','submit','mouseover','dblclick'andmore.
Whatisn'tinstantlyobviousisthatunderthebonnet,BackboneusesjQuery's.delegate()toprovideinstantsupportforeventdelegationbutgoesalittlefurther,extendingitsothat'this'alwaysreferstothecurrentviewobject.Theonlythingtoreallykeepinmindisthatanystringcallbacksuppliedtotheeventsattributemusthaveacorrespondingfunctionwiththesamenamewithinthescopeofyourviewotherwiseyoumayincurexceptions.
WhenlearninghowtouseBackbone,oneimportantareathatthatisverycommonlyoverlookedintutorialsisnamespacing.IfyoualreadyhaveexperiencewithhowtonamespaceinJavaScript,thefollowingsectionwillprovidesomeadviceonhowtospecificallyapplyconceptsyouknowtoBackbone,howeverIwillalsobecoveringexplanationsforbeginnerstoensureeveryoneisonthesamepage.
Thebasicideaaroundnamespacingistoavoidcollisionswithotherobjectsorvariablesintheglobalnamespace.They'reimportantasit'sbesttosafeguardyourcodefrombreakingintheeventofanotherscriptonthepageusingthesamevariablenamesasyouare.Asagood'citizen'oftheglobalnamespace,it'salsoimperativethatyoudoyourbesttosimilarlynotpreventotherdeveloper'sscriptsexecutingduetothesameissues.
Inthissectionwe'llbetakingalookshortlyatsomeexamplesofhowyoucannamespaceyourmodels,views,routersandothercomponentsspecifically.Thepatternswe'llbeexaminingare:
OnepopularpatternfornamespacinginJavaScriptisoptingforasingleglobalvariableasyourprimaryobjectofreference.Askeletonimplementationofthiswherewereturnanobjectwithfunctionsandpropertiescanbefoundbelow:
varmyApplication=(function(){function(){...},return{...}})();whichyou'relikelytohaveseenbefore.ABackbone-specificexamplewhichmaybemoreusefulis:
varmyViews=(function(){return{PhotoView:Backbone.View.extend({..}),GalleryView:Backbone.View.extend({..}),AboutView:Backbone.View.extend({..});//etc.};})();Herewecanreturnasetofviewsorevenanentirecollectionofmodels,viewsandroutersdependingonhowyoudecidetostructureyourapplication.Althoughthisworksforcertainsituations,thebiggestchallengewiththesingleglobalvariablepatternisensuringthatnooneelsehasusedthesameglobalvariablenameasyouhaveinthepage.
Onesolutiontothisproblem,asmentionedbyPeterMichaux,istouseprefixnamespacing.It'sasimpleconceptatheart,buttheideaisyouselectabasicprefixnamespaceyouwishtouse(inthisexample,myApplication_)andthendefineanymethods,variablesorotherobjectsaftertheprefix.
varmyApplication_photoView=Backbone.View.extend({}),myApplication_galleryView=Backbone.View.extend({});Thisiseffectivefromtheperspectiveoftryingtolowerthechancesofaparticularvariableexistingintheglobalscope,butrememberthatauniquelynamedobjectcanhavethesameeffect.Thisaside,thebiggestissuewiththepatternisthatitcanresultinalargenumberofglobalobjectsonceyourapplicationstartstogrow.
Note:Thereareseveralothervariationsonthesingleglobalvariablepatternoutinthewild,howeverhavingreviewedquiteafew,IfelttheseappliedbesttoBackbone.
Objectliteralshavetheadvantageofnotpollutingtheglobalnamespacebutassistinorganizingcodeandparameterslogically.They'rebeneficialifyouwishtocreateeasily-readablestructuresthatcanbeexpandedtosupportdeepnesting.Unlikesimpleglobalvariables,objectliteralsoftenalsotakeintoaccounttestsfortheexistenceofavariablebythesamenamesothechancesofcollisionoccurringaresignificantlyreduced.
Thecodeattheverytopofthenextsampledemonstratesthedifferentwaysinwhichyoucanchecktoseeifanamespacealreadyexistsbeforedefiningit.IcommonlyuseOption3.
/*Doesn'tcheckforexistenceofmyApplication*/varmyApplication={};/*Doescheckforexistence.Ifalreadydefined,weusethatinstance.Option1:if(!MyApplication)MyApplication={};Option2:varmyApplication=myApplication=myApplication||{}Option3:varmyApplication=myApplication||{};Wecanthenpopulateourobjectliteraltosupportmodels,viewsandcollections(oranydata,really):*/varmyApplication={models:{},views:{pages:{}},collections:{}};Onecanalsooptforaddingpropertiesdirectlytothenamespace(suchasyourviews,inthefollowingexample):
varmyGalleryViews=myGalleryViews||{};myGalleryViews.photoView=Backbone.View.extend({});myGalleryViews.galleryView=Backbone.View.extend({});Thebenefitofthispatternisthatyou'reabletoeasilyencapsulateallofyourmodels,views,routersetc.inawaythatclearlyseparatesthemandprovidesasolidfoundationforextendingyourcode.
Thispatternhasanumberofusefulapplications.It'softenofbenefittodecouplethedefaultconfigurationforyourapplicationintoasingleareathatcanbeeasilymodifiedwithouttheneedtosearchthroughyourentirecodebasejusttoalterthem-objectliteralsworkgreatforthispurpose.Here'sanexampleofahypotheticalobjectliteralforconfiguration:
varmyConfig={language:'english',defaults:{enableGeolocation:true,enableSharing:false,maxPhotos:20},theme:{skin:'a',toolbars:{index:'ui-navigation-toolbar',pages:'ui-custom-toolbar'}}}NotethattherearereallyonlyminorsyntacticaldifferencesbetweentheobjectliteralpatternandastandardJSONdataset.IfforanyreasonyouwishtouseJSONforstoringyourconfigurationsinstead(e.g.forsimplerstoragewhensendingtotheback-end),feelfreeto.
Anextensionoftheobjectliteralpatternisnestednamespacing.It'sanothercommonpatternusedthatoffersalowerriskofcollisionduetothefactthatevenifanamespacealreadyexists,it'sunlikelythesamenestedchildrendo.
Doesthislookfamiliar
YAHOO.util.Dom.getElementsByClassName('test');Yahoo'sYUIusesthenestedobjectnamespacingpatternregularlyandevenDocumentCloud(thecreatorsofBackbone)usethenestednamespacingpatternintheirmainapplications.AsampleimplementationofnestednamespacingwithBackbonemaylooklikethis:
vargalleryApp=galleryApp||{};/*performsimilarcheckfornestedchildren*/galleryApp.routers=galleryApp.routers||{};galleryApp.model=galleryApp.model||{};galleryApp.model.special=galleryApp.model.special||{};/*routers*/galleryApp.routers.Workspace=Backbone.Router.extend({});galleryApp.routers.PhotoSearch=Backbone.Router.extend({});/*models*/galleryApp.model.Photo=Backbone.Model.extend({});galleryApp.model.Comment=Backbone.Model.extend({});/*specialmodels*/galleryApp.model.special.Admin=Backbone.Model.extend({});Thisisbothreadable,organizedandisarelativelysafewayofnamespacingyourBackboneapplicationinasimilarfashiontowhatyoumaybeusedtoinotherlanguages.
Theonlyrealcaveathoweveristhatitrequiresyourbrowser'sJavaScriptenginefirstlocatingthegalleryAppobjectandthendiggingdownuntilitgetstothefunctionyouactuallywishtouse.
Thiscanmeananincreasedamountofworktoperformlookups,howeverdeveloperssuchasJuriyZaytsev(kangax)havepreviouslytestedandfoundtheperformancedifferencesbetweensingleobjectnamespacingvsthe'nested'approachtobequitenegligible.
Reviewingthenamespacepatternsabove,theoptionthatIwouldpersonallyusewithBackboneisnestedobjectnamespacingwiththeobjectliteralpattern.
Singleglobalvariablesmayworkfineforapplicationsthatarerelativelytrivial,however,largercodebasesrequiringbothnamespacesanddeepsub-namespacesrequireasuccinctsolutionthatpromotesreadabilityandscales.IfeelthispatternachievesalloftheseobjectiveswellandisaperfectcompanionforBackbonedevelopment.
ItworksverywellwithBackbone,Underscore,jQueryandCoffeeScriptandisevenusedbycompaniessuchasRedBullandJimBean.Youmayhavetoupdateanythirdpartydependencies(e.g.latestjQueryorZepto)whenusingit,butotherthanthatitshouldbefairlystabletouserightoutofthebox.
Brunchcaneasilybeinstalledviathenodejspackagemanagerandtakesjustlittletonotimetogetstartedwith.IfyouhappentouseVimorTextmateasyoureditorofchoice,youmaybehappytoknowthattherearealsoBrunchbundlesavailableforboth.
AsThomasDavishaspreviouslynoted,Backbone.js'sMVCisalooseinterpretationoftraditionalMVC,somethingcommontomanyclient-sideMVCsolutions.Backbone'sviewsarewhatcouldbeconsideredawrapperfortemplatingsolutionssuchastheMustache.jsandBackbone.ViewistheequivalentofacontrollerintraditionalMVC.Backbone.Modelishoweverthesameasaclassical'model'.
WhilstBackboneisnottheonlyclient-sideMVCsolutionthatcouldusesomeimprovementsinit'snamingconventions,Backbone.Controllerwasprobablythemostcentralsourceofsomeconfusionbuthasbeenrenamedarouterinmorerecentversions.Thiswon'tpreventyoufromusingBackboneeffectively,howeverthisisbeingpointedoutjusttohelpavoidanyconfusionifforanyreasonyouopttouseanolderversionoftheframework.
TheofficialBackbonedocsdoattempttoclarifythattheirroutersaren'treallytheCinMVC,butit'simportanttounderstandwherethesefitratherthanconsideringclient-sideMVCa1:1equivalenttothepatternyou'veprobablyseeninserver-sidedevelopment.
AndrewdeAndradehaspointedoutthatDocumentCloudthemselvesusuallyonlyuseasinglecontrollerinmostoftheirapplications.You'reverylikelytonotrequiremorethanoneortworoutersinyourownprojectsasthemajorityofyourapplicationroutingcanbekeptorganizedinasinglecontrollerwithoutitgettingunwieldy.
BackbonecanbeusedforbuildingbothtrivialandcomplexapplicationsasdemonstratedbythemanyexamplesAshkenashasbeenreferencingintheBackbonedocumentation.AswithanyMVCframeworkhowever,it'simportanttodedicatetimetowardsplanningoutwhatmodelsandviewsyourapplicationreallyneeds.Divingstraightintodevelopmentwithoutdoingthiscanresultineitherspaghetticodeoralargerefactorlateronandit'sbesttoavoidthiswherepossible.
Attheendoftheday,thekeytobuildinglargeapplicationsisnottobuildlargeapplicationsinthefirstplace.IfyouhoweverfindBackbonedoesn'tcutitforyourrequirementsIstronglyrecommendcheckingoutJavaScriptMVCorSproutCoreasthesebothofferalittlemorethanBackboneoutofthebox.DojoandDojoMobilemayalsobeofinterestasthesehavealsobeenusedtobuildsignificantlycomplexappsbyotherdevelopers.
OneofthegreatthingsaboutjQueryMobileisthatitallowsyoutoeasilycreatefunctionalmock-upsofyourintendedmobileUIsimplyusingmark-upanddataattributes.SimilartoDojoMobile,jQueryMobilecomeswithalargesuiteofpre-themedcomponentsandUIwidgetsthatcanbeusedtostructureacompleteinterfacewithoutneedingtotouchagreatdealofCSSduringtheprototypingphase.
ThismeansthatratherthanoptingtowireframeyourapplicationusingpencilsketchesoratoollikeBalsamiq/MockFlow,thesameamountoftimecanbeinvestedincreatingamock-upwhichcanberendertestedcross-platformandcross-devicepriortocodinganyreallogicforyourwebapp.Wecantheneasilywritethemodels,viewlogicandroutersforourapplicationandsimplytiethemtotheprototypetocompletetheproject.
We'regoingtobebuildingacompletemobilephotosearchapplicationthatusestheFlickrAPIandprovidesviewsforresults,individualphotos,optionsandsharing-allwhichwillbebookmarkable.InPart2ofthetutorial,we'reactuallygoingtowritetheBackbone.jscodethatpowerstherestoftheappbutfornow,it'simperativethatwegettheprototyperight.Sothatwehavesomethingtoreferenceeasily,I'vedecidedtocalltheapplicationFlickly.
Therealityishoweverthatwedon'talwayshavethescopetocreateper-deviceexperiences,sotoday'sapplicationwillattempttooptimizeforthedevicesorbrowsersmostlikelytoaccessit.It'sessentialthatthecontenttheapplicationexposesisreadilyavailabletoallusers,regardlessofwhatbrowsertheyareaccessingitfromsothisprinciplewillbekeptinmindduringthedesignphase.We'llbeaimingtocreateasolutionthat'sresponsivefromthestartandjQueryMobilesimplifiesalotoftheactualheavyliftinghereasitsupportsresponsivelayoutsoutofthebox(includingbrowsersthatdon'tsupportmediaqueries).
Let'simaginethathypothetically,ouranalyticsstatethat50%oftheuserswhocometoFlickraccessitviatheiPhone,10%viatheiPadandtheother40%viathedesktop.Ifwe'regoingtobuildawebapplicationcateredtomobile,itwouldthusmakesensetoensureiPhoneusersgetanoptimizedexperiencewithiPadandotherdevices(Blackberryetc.)gettinganexperiencethatlooksacceptable.
WithjQueryMobile,thismayjustmeanviewsbeingstretchedorwidenedalittlemore,buttheoverallapplicationwillstillremainfullyusable.
Inmyopinion,landing/firstscreenofamobileapplicationshouldfocusonencouragingengagementwithyourapporservice'sprimaryfunction.Keepitverysimple.WithFlickr,thisisrelativelystraight-forward-search.Oncetheprimaryfunctionhasbeenidentified,avoidthetemptationtoover-clutteryourUIforthesakeofmakingitlookmoreadvancedthanithasto.FlickrofferanumberofadvancedsearchoptionsthroughtheirAPIandIinitiallythoughtincludingsomeoftheseoptionsonthesamepagewouldmaketheuserfeelmoreempowered.
Theproblemwiththisassumptionisthatthemajorityofusersjustwantaneasywaytosearchforphotosusingkeywords.That'sallthefirstscreenneedstooffer-asearchbox.Iendedupmovingalloftheadvancedsearchoptionstoanoptionspage,butasit'sjustaclickaway,advanceduserslikelywon'tmindthesmallamountofextraworkrequiredtousethosefeatures.Afixed-positionnavigationtoolbarthatappearsontheverybottomeachviewensuresthatgettingtowhereyouneedtoseldomtakesmorethanatouch.
Thenextconcernwashowtodisplaysearchresults.Inmyfirstdraftoftheapplication,IthoughtthatmimickingFlickr'sdesktopexperience(theNxMimagegridfoundinresults)wouldgiveuserssomethingfamiliarthatthey'dbecomfortablewith.Wrongagain.Onamobiledevice,you'reusuallyconstrainedbylimiteddimensionsandshouldattempttoofferanexperiencethatworksbestforthisscenarioratherthanassumingonethatworkswellelsewhere.It'sadifferentparadigm.Thegridonmobilesufferedfromanumberofissueslikelackofreadablemeta-dataanddifficultyselecting(touching)imagesasthethumbnailswerequitesmall.
WhatIoptedforinsteadwasajQueryMobileListViewwhichwascreatedspecificallyforthepurposesofdisplayingalistofthumbnailswithadescription.Eachthumbnailisgivenit'sownrowonthescreenwithmeta-datapositionedtotherightandit'sextremelyeasytonavigatearound.ThejQueryMobiledesignteamreallythoughtthisthroughanditshows.Byusingheader-positionedpreviousandnextbuttons,Iwasthenabletogiveusersaneasywaytonavigatethroughpaginatedresultswithoutlimitingtheirexperienceinanyway.Asmentioned,eachoftheseresultpagescanbebookmarkedandmaintaininformationaboutthesearchtypeselected,pagenumberandkeywordschosen.
Anotherimportantviewwasthatforasinglephoto.TheFlickrAPIhasdistinctcallsrequiredforbothsearchresultsandsinglephotos(withfullmeta-data)soitmadesensetoofferaviewthatcouldprovidealloftheavailableinformationaboutanimagetotheuseriftheyclickedonaparticularsearchresult.Inthiscase,thedesktopexperiencewasn'toverlycomplex-displayalargeimagewithcaptionsbeneathit.PortingthisideaovertojQueryMobile,alargerimagewasdisplayedatthetopofaphotoviewwithaninlinelist(resemblinga1-columntable)forthemeta-data.Itwasclean,easytoreadandworkedwellonalldevices.
Ichosenottoaccessthecommentssectionofthereturneddata-setasinmyopinion,thiswouldhavelettofurtherviewsforuserprofiles,userpagesanditemsagain,outsidethescopeofthisproject.Togivetheuseranopportunitytoviewaricherexperienceiftheysochoose,eachsinglephotopagealsoincludesalinktoit'scorrespondingpageonFlickr.Thiswaytheuserdoesn'tfeeloverlylockedintotheapplicationiftheywanttoaccessacompleteviewoftheoriginaldata(withcommentsandthecompletesiteexperience).A'share'pagewithlinkstoposttheitemtoFacebookandTwitterwasalsoadded.
Anumberofotherpageswerecreatedasapartoftheapplication.Theseincludedabout,sourceandhelp.Unliketheotherviewsoftheapplicationthatwererequiredagreatdealofthought,eachofthesepagessimplyusedvariationsofthejQueryMobilelistoptionstorenderdatainareadable,rowformat.Youcanseescreenshotsofsomeofthesebelow,includingsamplesofhowtheotherpagesthatweremockedupmightlookinthefinalapplication.
Somedevelopersmayfindthedescriptionsofmythoughtprocessaroundthemobileapplicationminimalist,butIbelievethisisparamounttocreatinganexperiencethatemphasizesusabilityandappropriatenessfortheintendedaudience'sdeviceofchoiceoverotherfactors.It'sawayofthinkingthathasworkedformobileapplicationsI'vecreatedinthepast,howeverI'malwaysopentoconsideringotherapproachesifaccompaniedbyasoundargument:)
That'sitforPart1ofthistutorial.Ihopeit'scomeinuseful.InPart2,we'regoingtojumprightintosomeheavyBackbone.jsdevelopment,usingtheconceptsthathavebeenintroducedinthisfirstparttocreateafullyfunctionalmobilewebapplicationthatwillworkcross-browserandcross-device.Seeyouagainsoon!
AddyOsmaniisaUserInterfaceDeveloperfromLondon,England.Anavidblogger,hehasapassionforencouragingbestpracticesinclient-sidedevelopment,inparticularwithrespecttoJavaScriptandjQuery.HeenjoysevangelizingthelatteroftheseandisonthejQueryBugtriage,APIandfront-endteams.AddyworksatAOLwherehe'saJavaScriptdeveloperontheirEuropeanProducts&Innovationteam.