Enkelt side ToDo-applikasjon med Backbone.js

Backbone.js er et JavaScript-rammeverk for å bygge fleksible webapplikasjoner. Den leveres med modeller, samlinger, visninger, hendelser, rutere og noen andre flotte funksjoner. I denne artikkelen vil vi utvikle en enkel ToDo-applikasjon som støtter å legge til, redigere og fjerne oppgaver. Vi bør også kunne markere en oppgave som ferdig og arkiver den. For å holde denne postlengden rimelig, vil vi ikke inkludere noen kommunikasjon med en database. Alle dataene blir lagret på klientsiden.

Setup

Her er filstrukturen som vi skal bruke:

css └── styles.css js └─ - samlinger └── ToDos.js └─ - modeller └── ToDo.js └─ - leverandør └── backbone.js └── jquery-1.10.2.min.js └─ - underscore.js └─ - visninger └── App.js └─ - index.html 

Det er få ting som er åpenbare, som /css/styles.css og /index.html. De inneholder CSS-stilene og HTML-oppslaget. I sammenheng med Backbone.js er modellen et sted hvor vi beholder våre data. Så, våre ToDos vil bare være modeller. Og fordi vi vil ha mer enn en oppgave, vil vi organisere dem inn i en samling. Forretningslogikken er fordelt mellom visningene og hovedprogrammets fil, App.js. Backbone.js har bare en hard avhengighet - Underscore.js. Rammen spiller også veldig bra med jQuery, så de går begge til selger katalogen. Alt vi trenger nå er bare et lite HTML-oppslag, og vi er klare til å gå.

   Mine TODOer    

Som du kan se, inkluderer vi alle de eksterne JavaScript-filene mot bunnen, da det er en god praksis å gjøre dette på slutten av kroppsmerket. Vi forbereder også oppstart av applikasjonen. Det er container for innholdet, en meny og en tittel. Hovednavigasjonen er et statisk element, og vi kommer ikke til å endre det. Vi erstatter innholdet i tittelen og div under den.

Planlegger programmet

Det er alltid godt å ha en plan før vi begynner å jobbe med noe. Backbone.js har ikke en super streng arkitektur, som vi må følge. Det er en av fordelene ved rammen. Så, før vi starter med implementeringen av forretningslogikken, la oss snakke om grunnlaget.

navne

En god praksis er å sette koden i sitt eget omfang. Det er ikke en god ide å registrere globale variabler eller funksjoner. Det vi skal opprette, er en modell, en samling, en ruter og noen Backbone.js-visninger. Alle disse elementene skal leve i et privat rom. App.js vil inneholde klassen som holder alt.

// App.js var app = (funksjon () var api = visninger: , modeller: , samlinger: , innhold: null, router: null, todos: null, init: this.content = $ ("# content");, changeContent: funksjon (el) this.content.empty (). append (el); return dette;, tittel: funksjon (str) $ ("h1 ") .text (str); returner dette;; Var ViewsFactory = ; var Router = Ryggrad.Router.extend (); api.router = ny ruter (); return api;) 

Ovenfor er en typisk implementering av det avslørende modulmønsteret. De api variabel er gjenstanden som returneres og representerer klassens offentlige metoder. De visninger, modeller og samlinger Egenskaper vil fungere som innehavere for klassene returnert av Backbone.js. De innhold er et jQuery-element som peker på hovedbrukerens grensesnittbeholder. Det er to hjelpemetoder her. Den første oppdaterer den beholderen. Den andre setter tittelen på siden. Da definerte vi en modul som ble kalt ViewsFactory. Det vil levere våre synspunkter og på slutten opprettet vi ruteren.

Du kan spørre, hvorfor trenger vi en fabrikk for visningene? Vel, det er noen vanlige mønstre mens du jobber med Backbone.js. En av dem er relatert til etableringen og bruken av visningene.

var ViewClass = Backbone.View.extend (/ * logikk her * /); var view = new ViewClass (); 

Det er godt å initialisere visningene bare én gang og la dem være i live. Når dataene er endret, kaller vi vanligvis metoder for visningen og oppdaterer innholdet av det el gjenstand. Den andre svært populære tilnærmingen er å gjenskape hele visningen eller erstatte hele DOM-elementet. Det er imidlertid ikke veldig bra fra et ytelsesperspektiv. Så, vi ender opp med en verktøysklasse som skaper en forekomst av visningen og returnerer den når vi trenger den.

Komponenter Definisjon

Vi har et navneområde, så nå kan vi begynne å lage komponenter. Slik ser hovedmenyen ut:

// views / menu.js app.views.menu = Ryggrad.View.extend (initialiser: funksjon () , gjengiv: funksjon () ); 

Vi opprettet en eiendom som heter Meny som holder klassen av navigasjonen. Senere kan vi legge til en metode i fabrikkmodulen som skaper en forekomst av den.

var ViewsFactory = meny: funksjon () hvis (! this.menuView) this.menuView = new api.views.menu (el: $ ("# menu"));  returner dette.menuView; ; 

Over er hvordan vi skal håndtere alle visningene, og det vil sikre at vi bare får en og samme forekomst. Denne teknikken fungerer bra, i de fleste tilfeller.

Strømme

Inngangspunktet for appen er App.js og dets i det metode. Dette er hva vi skal ringe i på Last handler av vindu gjenstand.

window.onload = function () app.init ();  

Deretter tar den definerte ruteren kontroll. Basert på nettadressen, bestemmer den hvilken håndterer som skal utføres. I Backbone.js har vi ikke den vanlige modell-View-Controller-arkitekturen. Kontrolleren mangler, og det meste av logikken blir lagt inn i visningene. Så i stedet leder vi modellene direkte til metoder, inne i visningene og får en øyeblikkelig oppdatering av brukergrensesnittet, når dataene har endret seg.

Administrere dataene

Det viktigste i vårt lille prosjekt er dataene. Våre oppgaver er hva vi skal klare, så la oss starte derfra. Her er vår modelldefinisjon.

// modeller / ToDo.js app.models.ToDo = Backbone.Model.extend (standard: title: "ToDo", arkivert: false, ferdig: false); 

Bare tre felt. Den første inneholder teksten til oppgaven, og de to andre er flagg som definerer statusen til posten.

Hver ting inne i rammen er faktisk en event dispatcher. Og fordi modellen er endret med settere, vet rammen når dataene er oppdatert og kan varsle resten av systemet for det. Når du har bundet noe til disse varslene, vil din søknad reagere på endringene i modellen. Dette er en veldig kraftig funksjon i Backbone.js.

Som jeg sa i begynnelsen, vil vi ha mange poster, og vi vil organisere dem inn i en samling som heter Todos.

// samlinger / ToDos.js app.collections.ToDos = Backbone.Collection.extend (initialiser: funksjon () this.add (title: "Lær JavaScript grunnleggende"); this.add (title: "Go til backbonejs.org "); this.add (title:" Utvikle et ryggradsprogram ");, modell: app.models.ToDo up: funksjon (indeks) hvis (indeks> 0) var tmp = this.models [index-1]; this.models [index-1] = this.models [index]; this.models [index] = tmp; this.trigger ("change");, ned: funksjon indeks) hvis (indeks < this.models.length-1)  var tmp = this.models[index+1]; this.models[index+1] = this.models[index]; this.models[index] = tmp; this.trigger("change");  , archive: function(archived, index)  this.models[index].set("archived", archived); , changeStatus: function(done, index)  this.models[index].set("done", done);  ); 

De initial Metoden er inngangspunktet for samlingen. I vårt tilfelle har vi lagt til noen få oppgaver som standard. Selvfølgelig i den virkelige verden, vil informasjonen komme fra en database eller et annet sted. Men for å holde deg fokusert, vil vi gjøre det manuelt. Den andre tingen som er typisk for samlinger, setter inn modell eiendom. Det forteller klassen hvilken type data som lagres. Resten av metodene implementerer tilpasset logikk, relatert til funksjonene i vår søknad. opp og ned funksjoner endrer rekkefølgen til ToDos. For å forenkle ting identifiserer vi hver ToDo med bare en indeks i samlingens array. Dette betyr at hvis vi ønsker å hente en bestemt post, bør vi peke på indeksen. Så, bestillingen skifter bare elementene i en matrise. Som du kanskje gjetter fra koden ovenfor, this.models er samlingen som vi snakker om. arkiv og change sett egenskapene til det gitte elementet. Vi legger disse metodene her, fordi visningene vil ha tilgang til Todos samling og ikke direkte til oppgavene.

I tillegg trenger vi ikke å lage noen modeller fra app.models.ToDo klasse, men vi trenger å lage en forekomst fra app.collections.ToDos samling.

// App.js init: funksjon () this.content = $ ("# content"); this.todos = nye api.collections.ToDos (); returnere dette;  

Viser vår første visning (hovednavigasjon)

Det første vi må vise er hovedapplikasjonens navigasjon.

// view / menu.js app.views.menu = Backbone.View.extend (mal: _.template ($ ("# tpl-menu"). html ()), initialiser: funksjon () this.render ();, gjengi: funksjon () this. $ el.html (this.template ());); 

Det er bare ni kodelinjer, men det er mange kule ting som skjer her. Den første er å sette inn en mal. Hvis du husker, la vi Underscore.js til vår app? Vi skal bruke sin templerende motor, fordi det fungerer bra og det er enkelt nok å bruke.

_.template (templateString, [data], [settings]) 

Hva du har på slutten, er en funksjon som aksepterer et objekt som holder informasjonen din i nøkkelverdierpar og templateString er HTML markup. Ok, så det aksepterer en HTML-streng, men hva er det $ ( "# TPL-meny"). Html () gjør det? Når vi utvikler et lite enkelttsidsprogram, legger vi vanligvis maler direkte inn på siden slik:

// index.html  

Og fordi det er et skript, er det ikke vist for brukeren. Fra et annet synspunkt er det en gyldig DOM-node, slik at vi kan få innholdet sitt med jQuery. Så, det korte kuttet over tar bare innholdet i skriptet.

De gjengi Metoden er veldig viktig i Backbone.js. Det er funksjonen som viser dataene. Normalt binder du hendelsene som er trukket av modellene direkte til den metoden. Men for hovedmenyen trenger vi ikke slik oppførsel.

. Dette $ el.html (this.template ()); 

dette. $ el er et objekt som er opprettet av rammen, og hver visning har det som standard (det er en $ foran el fordi vi har med jQuery inkludert). Og som standard er det tomt

. Selvfølgelig kan du endre det ved å bruke TagNavn eiendom. Men det som er viktigere her er at vi ikke tilordner en verdi direkte til det aktuelle objektet. Vi endrer ikke det, vi endrer bare innholdet sitt. Det er stor forskjell mellom linjen over og denne neste:

dette. $ el = $ (this.template ()); 

Poenget er at hvis du vil se endringene i nettleseren, bør du ringe gjengivelsesmetoden før, for å legge til visningen til DOM. Ellers vil bare den tomme div bli festet. Det er også et annet scenario hvor du har nestede visninger. Og fordi du endrer eiendommen direkte, blir ikke foreldrekomponenten oppdatert. De bundet hendelsene kan også bli ødelagte, og du må feste lytterne igjen. Så, du bør bare endre innholdet av dette. $ el og ikke egenskapens verdi.

Utsikten er nå klar, og vi må initialisere den. La oss legge den til i vår fabrikkmodul:

// App.js var ViewsFactory = meny: funksjon () hvis (! This.menuView) this.menuView = nye api.views.menu (el: $ ("# menu"));  returner dette.menuView; ; 

På slutten kaller du bare Meny metode i bootstrapping-området:

// App.js init: funksjon () this.content = $ ("# content"); this.todos = nye api.collections.ToDos (); ViewsFactory.menu (); returnere dette;  

Legg merke til at mens vi lager en ny forekomst fra navigasjonsklassen, overfører vi et allerede eksisterende DOM-element $ ( "# Meny"). Så dette. $ el eiendom i utsikten er faktisk peker på $ ( "# Meny").

Legge til ruter

Backbone.js støtter push-tilstand operasjoner. Med andre ord kan du manipulere nettleserens nettadresse og reise mellom sider. Vi vil imidlertid holde fast med de gode URL-adressene for gamle hashtyper, for eksempel / # Redigere / 3.

// App.js var Router = Ryggrad.Router.extend (ruter: "arkiv": "arkiv", "nytt": "newToDo", "edit /: index": "editToDo", "delete / ":" delteToDo "," ":" liste ", liste: funksjon (arkiv) , arkiv: funksjon () , newToDo: funksjon () , editToDo: funksjon (indeks) , delteToDo: funksjon (indeks) ); 

Over er ruteren vår. Det er fem ruter definert i et hashobjekt. Nøkkelen er hva du vil skrive inn i nettleserens adresselinje og verdien er funksjonen som vil bli kalt. Legg merke til at det er : indeks på to av ruterne. Det er syntaksen du må bruke hvis du vil støtte dynamiske nettadresser. I vårt tilfelle, hvis du skriver # Redigere / 3 de editToDo vil bli utført med parameter indeks = 3. Den siste raden inneholder en tom streng som betyr at den håndterer hjemmesiden til søknaden vår.

Viser en liste over alle oppgavene

Så langt hva vi har bygget er hovedvisningen for prosjektet vårt. Den henter dataene fra samlingen og skriver den ut på skjermen. Vi kunne bruke samme visning for to ting - viser alle aktive ToDos og viser de som er arkivert.

Før vi fortsetter med implementeringen av listevisningen, la oss se hvordan den faktisk er initialisert.

// i App.js visninger fabrikkliste: funksjon () if (! this.listView) this.listView = new api.views.list (model: api.todos);  returner dette.listView;  

Legg merke til at vi passerer inn i samlingen. Det er viktig fordi vi senere bruker this.model for å få tilgang til lagrede data. Fabrikken returnerer listevisningen, men ruteren er fyren som må legge den til siden.

// i App.js ruter liste: funksjon (arkiv) var view = ViewsFactory.list (); api .title (arkiv? "Arkiv:": "Din ToDos:") .changeContent (se. $ el); view.setMode (arkiv? "arkiv": null) .render ();  

For nå, metoden liste i ruteren kalles uten noen parametere. Så visningen er ikke inne arkiv modus, vil det bare vise de aktive ToDos.

// visninger / list.js app.views.list = Backbone.View.extend (modus: null, hendelser: , initialiser: funksjon () var handler = _.bind (this.render, this); dette .model.bind ('change', handler); this.model.bind ('add', handler); this.model.bind ('remove', handler);, gjengi: funksjon () , prioritet: funksjon (e) , prioritetDown: funksjon (e) , arkiv: funksjon (e) , changeStatus: funksjon (e) , setMode: funksjon (modus) this.mode = mode; ); 

De modus Egenskapen vil bli brukt under gjengivelsen. Hvis verdien er mode = "arkiv" da vil bare de arkiverte ToDos bli vist. De arrangementer er et objekt som vi vil fylle med en gang. Det er stedet der vi plasserer DOM-hendelser kartlegging. Resten av metodene er svar på brukerinteraksjonen, og de er direkte knyttet til de nødvendige funksjonene. For eksempel, priorityUp og priorityDown endrer bestilling av ToDos. arkiv flytter elementet til arkivområdet. change markerer bare ToDo som gjort.

Det er interessant hva som skjer inni initial metode. Tidligere sa vi at du normalt vil binde endringene i modellen (samlingen i vårt tilfelle) til gjengi visningsmetode. Du kan skrive this.model.bind ('change', this.render). Men veldig snart vil du legge merke til at dette søkeord, i gjengi Metoden vil ikke peke på selve utsikten. Det er fordi omfanget er endret. Som en løsning oppretter vi en handler med et allerede definert omfang. Det er hva Underscore er binde funksjonen brukes til.

Og her er implementeringen av gjengi metode.

// visninger / list.js gjengir: funksjon () ) var html = '
    ', selv = dette; this.model.each (funksjon (todo, indeks) if (self.mode === "archive"? todo.get ("arkivert") === true: todo.get ("arkivert") === falsk ) var template = _.template ($ ("# tpl-list-item"). html ()); html + = mal (tittel: todo.get ("title"), indeks: indeks, arkivLink: selv .mode === "arkiv"? "unarchive": "arkiv", ferdig: todo.get ("ferdig")? "ja": "nei", ferdigkontrollert: todo.get = "sjekket" ': "");); html + = '
'; . Dette $ el.html (html); this.delegateEvents (); returnere dette;

Vi slår gjennom alle modellene i samlingen og genererer en HTML-streng, som senere settes inn i visningens DOM-element. Det er få sjekker som skiller ToDos fra arkivert til aktivt. Oppgaven er merket som ferdig ved hjelp av en avkrysningsboks. Så for å indikere dette må vi passere en sjekket == "merket" attributt til det elementet. Du kan merke at vi bruker this.delegateEvents (). I vårt tilfelle er dette nødvendig, fordi vi fjerner og legger til visningen fra DOM. Ja, vi erstatter ikke hovedelementet, men hendelsene er fjernet. Det er derfor vi må fortelle Backbone.js å ​​feste dem igjen. Malen som brukes i koden ovenfor er:

// index.html  

Legg merke til at det er en CSS-klasse definert kalt gjort-ja, som maler ToDo med en grønn bakgrunn. Dessuten er det en rekke lenker som vi skal bruke for å implementere den nødvendige funksjonaliteten. De har alle dataattributter. Hovednoden til elementet, li, har data-indeks. Verdien av dette attributtet viser indeksen for oppgaven i samlingen. Legg merke til at de spesielle uttrykkene er pakket inn <%=… %> blir sendt til mal funksjon. Det er dataene som injiseres i malen.

Det er på tide å legge til noen hendelser i visningen.

// / view.list.js events: 'klikk på [data-up]': 'priorityUp', 'klikk på [data ned]': 'priorityDown', 'klikk på et [dataarkiv]': 'arkiv ',' klikk inntasting [datastatus] ':' changeStatus ' 

I Backbone.js er arrangementets definisjon bare en hash. Du skriver først navnet på hendelsen og deretter en velger. Verdiene av egenskapene er faktisk visningsmetodene.

// views / list.js priorityUp: funksjon (e) var index = parseInt (e.target.parentNode.parentNode.getAttribute ("data-index")); this.model.up (index); , prioritetDown: funksjon (e) var index = parseInt (e.target.parentNode.parentNode.getAttribute ("data-index")); this.model.down (index); , arkiv: funksjon (e) var index = parseInt (e.target.parentNode.parentNode.getAttribute ("data-index")); this.model.archive (this.mode! == "arkiv", indeks); , changeStatus: funksjon (e) var index = parseInt (e.target.parentNode.parentNode.getAttribute ("data-index")); this.model.changeStatus (e.target.checked, indeks);  

Her bruker vi e.target kommer inn til handleren. Det peker på DOM-elementet som utløste hendelsen. Vi får indeksen til klikket ToDo og oppdaterer modellen i samlingen. Med disse fire funksjonene har vi avsluttet vår klasse, og nå er dataene vist på siden.

Som vi nevnte ovenfor, vil vi bruke samme visning for Arkiv side.

liste: funksjon (arkiv) var view = ViewsFactory.list (); api .title (arkiv? "Arkiv:": "Din ToDos:") .changeContent (se. $ el); view.setMode (arkiv? "arkiv": null) .render (); , arkiv: funksjon () this.list (true);  

Over er samme rutehåndterer som før, men denne gangen med ekte som en parameter.

Legge til og redigere ToDos

Etter primeren i listevisningen kan vi opprette en annen som viser et skjema for å legge til og redigere oppgaver. Slik er denne nye klassen opprettet:

// App.js / views fabrikkform: funksjon () if (! This.formView) this.formView = new api.views.form (model: api.todos). På ("lagret", funksjon ( ) api.router.navigate ("", trigger: true);) returner this.formView;  

Stort sett det samme. Men denne gangen må vi gjøre noe når skjemaet er sendt inn. Og det er videre til brukeren på hjemmesiden. Som jeg sa, er hvert objekt som utvider Backbone.js-klasser, faktisk en hendelse-dispatcher. Det er metoder som og avtrekker som du kan bruke.

Før vi fortsetter med visningskoden, la oss se på HTML-malen:

 

Vi har en textarea og a knapp. Malen forventer a tittel parameter som skal være en tom streng, hvis vi legger til en ny oppgave.

// visninger / form.js app.views.form = Backbone.View.extend (indeks: falsk, hendelser: 'klikk-knapp': 'lagre', initialiser: funksjon () this.render (); , gjengivelse: funksjon (indeks) varmal, html = $ ("# tpl-form"). html (); hvis (typeof index == 'undefined') this.index = false; template = _.template html, title: ""); andre this.index = parseInt (indeks); this.todoForEditing = this.model.at (this.index); template = _.template ($ ("# tpl-form ") .html (), title: this.todoForEditing.get (" title ")); dette. $ el.html (mal); dette. $ el.find (" textarea ") .fokus (); this.delegateEvents (); return dette;, lagre: funksjon (e) e.preventDefault (); var title = dette. $ el.find ("textarea") .val (); if (title == "" ) alert ("Empty textarea!"; return; hvis (this.index! == false) this.todoForEditing.set ("title", tittel); else this.model.add (title: tittel); this.trigger ("saved");); 

Utsikten er bare 40 linjer med kode, men det gjør jobben sin bra. Det er bare en hendelse knyttet og dette er å klikke på lagre-knappen. Gjenvinningsmetoden virker annerledes avhengig av det bestått index parameter. For eksempel, hvis vi redigerer en ToDo, passerer vi indeksen og henter den eksakte modellen. Hvis ikke, er skjemaet tomt og en ny oppgave blir opprettet. Det er flere interessante punkter i koden ovenfor. Først, i gjengivelsen brukte vi .fokus() Metode for å bringe fokuset til skjemaet når visningen gjengis. Igjen delegateEvents funksjonen skal kalles, fordi skjemaet kunne løsnes og festes igjen. De lagre Metoden starter med e.preventDefault (). Dette fjerner standardoppførelsen til knappen, som i noen tilfeller kan sende inn skjemaet. Og til slutt, når alt er gjort, utløste vi lagret Hendelse som informerer omverdenen om at ToDo er lagret i samlingen.

Det er to metoder for ruteren som vi må fylle ut.

// App.js newToDo: funksjon () var view = ViewsFactory.form (); api.title ("Opprett ny ToDo:"). changeContent (se. $ el); view.render (), editToDo: funksjon (indeks) var view = ViewsFactory.form (); . Api.title ( "Edit:") changeContent (vis $ el.); view.render (index);  

Forskjellen mellom dem er at vi passerer i en indeks, hvis redigere /: indeks ruten er tilpasset. Og selvfølgelig er tittelen på siden endret tilsvarende.

Slette en post fra samlingen

For denne funksjonen trenger vi ikke en visning. Hele jobben kan gjøres direkte i ruterenes håndterer.

delteToDo: funksjon (indeks) api.todos.remove (api.todos.at (parseInt (indeks))); api.router.navigate ("", trigger: true);  

Vi kjenner indeksen til ToDo som vi vil slette. Det er en fjerne metode i samlingen klassen som aksepterer en modell objekt. På slutten, bare videresende brukeren til hjemmesiden, som viser den oppdaterte listen.

Konklusjon

Backbone.js har alt du trenger for å bygge en fullt funksjonell, enkeltside applikasjon. Vi kan til og med binde den til en REST back-end-tjeneste, og rammen vil synkronisere dataene mellom appen din og databasen. Den hendelsesdrevne tilnærmingen oppfordrer modulær programmering, sammen med en god arkitektur. Jeg bruker personlig Backbone.js til flere prosjekter, og det fungerer veldig bra.