Ettersom moderne webapplikasjoner gjør mer og mer på klientsiden (det faktum at vi nå omtaler dem som 'webapplikasjoner' i motsetning til 'nettsteder' er ganske fortellende), har det vært en økende interesse for klientside-rammer . Det er mange spillere i dette feltet, men for applikasjoner med mye funksjonalitet og mange bevegelige deler, skiller to av dem seg spesielt ut: Angular.js og Ember.js .
Vi har allerede publisert en omfattende [Angular.js tutorial] [https://www.toptal.com/angular-js/a-step-by-step-guide-to-your-first-angularjs-app], så vi ' kommer til å fokusere på Ember.js i dette innlegget, der vi bygger et enkelt Ember-program for å katalogisere musikksamlingen din. Du vil bli introdusert for rammeverkets viktigste byggesteiner og få et innblikk i designprinsippene. Hvis du vil se kildekoden mens du leser, er den tilgjengelig som rock-and-roll på Github .
Slik ser Rock & Roll-appen ut i den endelige versjonen:
Til venstre ser du at vi har en liste over artister og til høyre en liste over sanger av den valgte artisten (du kan også se at jeg har god musikksmak, men jeg går bort). Nye artister og sanger kan legges til ganske enkelt ved å skrive i tekstboksen og trykke på tilstøtende knapp. Stjernene ved siden av hver sang tjener til å rangere den, à la iTunes.
Vi kan dele opp appens rudimentære funksjonalitet i følgende trinn:
Vi har en lang vei å gå for å få dette til å fungere, så la oss begynne.
Et av de særtrekkene ved Ember er den store vekt det legger på nettadresser. I mange andre rammer mangler det enten å ha separate nettadresser for separate skjermer, eller blir brukt som en ettertanke. I Ember er ruteren - komponenten som administrerer nettadresser og overganger mellom dem - det sentrale stykket som koordinerer arbeid mellom byggesteiner. Derfor er det også nøkkelen til å forstå det indre arbeidet til Ember-applikasjoner.
Her er rutene for søknaden vår:
App.Router.map(function() { this.resource('artists', function() { this.route('songs', { path: ':slug' }); }); });
Vi definerer en ressursrute, artists
, og en songs
rute nestet inne i den. Denne definisjonen vil gi oss følgende ruter:
Jeg brukte det flotte Ember Inspector-pluginet (det eksisterer begge for Chrome og Firefox ) for å vise deg de genererte rutene på en lett lesbar måte. Her er de grunnleggende reglene for Ember-ruter, som du kan bekrefte for vår spesielle sak ved hjelp av tabellen ovenfor:
Det er en implisitt application
rute.
Dette blir aktivert for alle forespørsler (overganger).
Det er en implisitt index
rute.
Dette blir skrevet inn når brukeren navigerer til roten til applikasjonen.
Hver ressursrute oppretter en rute med samme navn og oppretter implisitt en indeksrute under den.
Denne indeksruten aktiveres når brukeren navigerer til ruten. I vårt tilfelle artists.index
blir utløst når brukeren navigerer til /artists
.
En enkel (ikke-ressurs) nestet rute vil ha sitt overordnede rutenavn som prefiks.
Ruten vi definerte som this.route('songs', ...)
vil ha artists.songs
som navnet. Det blir utløst når brukeren navigerer til /artists/pearl-jam
eller /artists/radiohead
.
Hvis stien ikke er gitt, antas den å være lik rutenavnet.
Hvis banen inneholder et :
, regnes det som et dynamisk segment .
Navnet som er tildelt det (i vårt tilfelle slug
) vil samsvare med verdien i riktig segment av url. slug
segmentet ovenfor vil ha verdien pearl-jam
, radiohead
eller annen verdi som ble hentet fra URL-en.
Som et første trinn bygger vi skjermen som viser listen over artister til venstre. Denne skjermen skal vises til brukerne når de navigerer til /artists/
:
For å forstå hvordan skjermen gjengis, er det på tide å introdusere et annet overordnet Ember-designprinsipp: konvensjon over konfigurasjon . I avsnittet ovenfor så vi at /artists
aktiverer artists
rute. Etter konvensjon, navnet på den ruten gjenstand er ArtistsRoute
. Det er dette ruteobjektets ansvar å hente data som appen kan gjengi. Det skjer i rutens modellkrok:
App.ArtistsRoute = Ember.Route.extend({ model: function() { var artistObjects = []; Ember.$.getJSON('http://localhost:9393/artists', function(artists) { artists.forEach(function(data) { artistObjects.pushObject(App.Artist.createRecord(data)); }); }); return artistObjects; } });
I dette utdraget blir dataene hentet via en XHR-samtale fra back-enden og - etter konvertering til et modellobjekt - presset til en matrise som vi senere kan vise. Imidlertid utvider ikke rutens respons til å gi displaylogikk, som håndteres av kontrolleren. La oss ta en titt.
Hmmm — faktisk trenger vi ikke definere kontrolleren på dette tidspunktet! Ember er smart nok til å generere kontrolleren når det trengs og still inn kontrolleren M.odel
attributt til returverdien til selve modellkroken, nemlig listen over artister. (Igjen, dette er et resultat av paradigmet ‘convention over configuration’.) Vi kan trappe ett lag ned og lage en mal for å vise listen:
{{#each model}} {{#link-to 'artists.songs' this class='list-group-item artist-link'}} {{name}} {{/link-to}} {{/each}} {{outlet}}
Hvis dette ser kjent ut, er det fordi Ember.js bruker Styr maler, som har en veldig enkel syntaks og hjelpere, men som ikke tillater ikke-triviell logikk (f.eks. ORing eller ANDing-vilkår i en betinget).
I malen ovenfor gjentas vi gjennom modellen (satt opp tidligere av ruten til en matrise som inneholder alle artister), og for hvert element i den gjengir vi en lenke som tar oss til artists.songs
rute for den artisten. Koblingen inneholder artistnavnet. #each
helper in Handlebars endrer omfanget i den til gjeldende vare, så {{name}}
vil alltid referere til navnet på artisten som for tiden er under iterasjon.
Et annet interessepunkt i utdraget ovenfor: {{outlet}}
, som spesifiserer spor i malen der innhold kan gjengis. Når du hekker ruter, blir malen for den ytre ressursruten gjengitt først, etterfulgt av den indre ruten, som gjengir malinnholdet til {{outlet}}
definert av den ytre ruten. Dette er akkurat det som skjer her.
Etter konvensjon gjengir alle ruter innholdet i malen med samme navn. Ovenfor er data-template-name
attributtet til malen ovenfor er artists
som betyr at den blir gjengitt for den ytre ruten, artists
. Den angir et utløp for innholdet i høyre panel, der den indre ruten, artists.index
gjengir innholdet:
Select an artist.
Oppsummert gjengir en rute (artists
) innholdet i venstre sidefelt, og modellen er listen over artister. En annen rute, artists.index
gjengir sitt eget innhold i sporet gitt av artists
mal. Det kan hente data for å fungere som modell, men i dette tilfellet er alt vi vil vise, statisk tekst, så vi trenger ikke.
Deretter ønsker vi å kunne lage artister, ikke bare se på en kjedelig liste.
Da jeg viste at artists
mal som gjengir listen over artister, jeg jukset litt. Jeg klippet ut den øverste delen for å fokusere på det som er viktig. Nå vil jeg legge til det tilbake:
{{input type='text' class='new-artist' placeholder='New Artist' value=newName}} Add ...
Vi bruker en Ember-hjelper, input
, med tekst for å gjengi en enkel tekstinntasting. I det, vi binde verdien av tekstinntastingen til newName
egenskapen til kontrolleren som tar sikkerhetskopi av denne malen, ArtistsController
. Som en konsekvens, når verdiegenskapen til inngangen endres (med andre ord når brukeren skriver inn tekst i den), blir newName
eiendom på kontrolleren vil bli holdt synkronisert.
Vi gjør det også kjent at createArtist
handling bør utløses når du klikker på knappen. Til slutt binder vi den deaktiverte egenskapen til knappen til disabled
kontrollerens eiendom. Så hvordan ser kontrolleren ut?
App.ArtistsController = Ember.ArrayController.extend({ newName: '', disabled: function() { return Ember.isEmpty(this.get('newName')); }.property('newName') });
newName
er satt til tomt i begynnelsen, noe som betyr at tekstinntastingen skal være tom. (Husker du hva jeg fortalte om bindinger? Prøv å endre newName
og se at den blir reflektert som teksten i inndatafeltet.)
disabled
er implementert slik at når det ikke er tekst i inntastingsboksen, kommer den til å returnere true
og dermed blir knappen deaktivert. .property
ring på slutten gjør dette til en “beregnet eiendom”, en annen deilig bit av Ember-kaken.
Beregnede egenskaper er egenskaper som er avhengige av andre egenskaper, som selv kan være “normale” eller beregnet. Ember cacher verdien av disse til en av de avhengige egenskapene endres. Den beregner deretter verdien av den beregnede eiendommen og cacher den igjen.
Her er en visuell fremstilling av prosessen ovenfor. For å oppsummere: når brukeren skriver inn navnet på en artist, blir newName
eiendomsoppdateringer, etterfulgt av disabled
eiendom, og til slutt legges kunstnerens navn til listen.
Tenk på det et øyeblikk. Ved hjelp av bindinger og beregnede egenskaper kan vi etablere (modell) data som én kilde til sannhet . Ovenfor utløser en endring i navnet på den nye artisten en endring i kontrolleregenskapen, som igjen utløser en endring i den deaktiverte eiendommen. Når brukeren begynner å skrive inn navnet på den nye artisten, blir knappen aktivert, som ved magi.
Jo større systemet er, jo mer gir vi gevinst fra ‘single source of truth’-prinsippet. Det holder koden vår ren og robust, og eiendomsdefinisjonene våre er mer deklarative.
Noen andre rammer legger også vekt på at modelldata skal være den eneste kilden til sannhet, men enten ikke gå så langt som Ember eller ikke gjøre en så grundig jobb. Vinklet har for eksempel toveisbindinger - men har ikke beregnede egenskaper. Det kan 'etterligne' beregnede egenskaper gjennom enkle funksjoner; Problemet her er at det ikke har noen måte å vite når man skal oppdatere en “beregnet eiendom” og dermed ty til skitten kontroll og i sin tur fører til tap av ytelse, spesielt kjent i større applikasjoner.
Hvis du ønsker å lære mer om emnet, anbefaler jeg deg å lese eviltrouts blogginnlegg for en kortere versjon eller dette Quora-spørsmålet for en lengre diskusjon der kjerneutviklere fra begge sider veier inn.
La oss gå tilbake for å se hvordan createArtist
handling blir opprettet etter at den er avfyrt (ved å trykke på knappen):
App.ArtistsRoute = Ember.Route.extend({ ... actions: { createArtist: function() { var name = this.get('controller').get('newName'); Ember.$.ajax('http://localhost:9393/artists', { type: 'POST', dataType: 'json', data: { name: name }, context: this, success: function(data) { var artist = App.Artist.createRecord(data); this.modelFor('artists').pushObject(artist); this.get('controller').set('newName', ''); this.transitionTo('artists.songs', artist); }, error: function() { alert('Failed to save artist'); } }); } } });
Handlingsbehandlere må pakkes inn i en actions
objekt og kan defineres på ruten, kontrolleren eller visningen. Jeg valgte å definere det på ruten her fordi resultatet av handlingen ikke er begrenset til kontrolleren, men heller 'global'.
Det er ikke noe fancy som skjer her. Etter at back-enden informerte oss om at lagringsoperasjonen var fullført, gjør vi tre ting, i rekkefølge:
newName
bindende, og sparer oss for å måtte manipulere DOM direkte.artists.songs
), og passerer den nyopprettede artisten som modell for den ruten. transitionTo
er måten å bevege seg mellom ruter internt. (Hjelper link-to
tjener til å gjøre det via brukerhandling.)Vi kan vise sangene til en artist enten ved å klikke på artistens navn. Vi sender også inn artisten som skal bli modellen for den nye ruten. Hvis modellobjektet dermed sendes inn, blir model
rutenes krok vil ikke bli kalt siden det ikke er behov for å løse modellen.
Den aktive ruten her er artists.songs
og dermed vil kontrolleren og malen være ArtistsSongsController
og henholdsvis artists/songs
Vi har allerede sett hvordan malen blir gjengitt til stikkontakten som tilbys av artists
mal slik at vi kan fokusere på bare malen:
(...) {{#each songs}} {{title}} {{view App.StarRating maxRating=5}} {{/each}}
Merk at jeg fjernet koden for å lage en ny sang, da den ville være nøyaktig den samme som for å lage en ny artist.
songs
egenskapen er satt opp i alle artistobjekter fra dataene som serveren returnerer. Den nøyaktige mekanismen som det gjøres, har liten interesse for den nåværende diskusjonen. Foreløpig er det tilstrekkelig for oss å vite at hver sang har tittel og rangering.
Tittelen vises direkte i malen, og vurderingen blir representert med stjerner via StarRating
utsikt. La oss se det nå.
Rangering av en sang faller mellom 1 og 5 og vises til brukeren gjennom en visning, App.StarRating
. Visninger har tilgang til sin kontekst (i dette tilfellet sangen) og kontrolleren deres. Dette betyr at de kan lese og endre egenskapene. Dette er i motsetning til en annen Ember-byggestein, komponenter, som er isolerte, gjenbrukbare kontroller med tilgang til bare det som er gitt inn til dem. (Vi kan også bruke en stjernerangeringskomponent i dette eksemplet.)
La oss se hvordan visningen viser antall stjerner og setter sangens rangering når brukeren klikker på en av stjernene:
App.StarRating = Ember.View.extend({ classNames: ['rating-panel'], templateName: 'star-rating', rating: Ember.computed.alias('context.rating'), fullStars: Ember.computed.alias('rating'), numStars: Ember.computed.alias('maxRating'), stars: function() { var ratings = []; var fullStars = this.starRange(1, this.get('fullStars'), 'full'); var emptyStars = this.starRange(this.get('fullStars') + 1, this.get('numStars'), 'empty'); Array.prototype.push.apply(ratings, fullStars); Array.prototype.push.apply(ratings, emptyStars); return ratings; }.property('fullStars', 'numStars'), starRange: function(start, end, type) { var starsData = []; for (i = start; i <= end; i++) { starsData.push({ rating: i, full: type === 'full' }); }; return starsData; }, (...) });
rating
, fullStars
og numStars
er beregnede egenskaper som vi tidligere har diskutert med disabled
egenskapen til ArtistsController
. Ovenfor brukte jeg en såkalt beregnet eiendomsmakro, omtrent et dusin av dem er definert i Ember. De gjør typiske beregnede egenskaper mer kortfattede og mindre feilutsatte (å skrive). Jeg satte rating
å være rangering av konteksten (og dermed sangen), mens jeg definerte både fullStars
og numStars
egenskaper slik at de leser bedre i sammenheng med stjernevurderingsmodulen.
stars
metoden er hovedattraksjonen. Den returnerer en rekke data for stjernene der hvert element inneholder rating
eiendom (fra 1 til 5) og et flagg (full
) for å indikere om stjernen er full. Dette gjør det veldig enkelt å gå gjennom dem i malen:
{{#each view.stars}}
{{/each}} Denne kodebiten inneholder flere punkter:
each
hjelper angir at den bruker en visningsegenskap (i motsetning til en egenskap på kontrolleren) ved å prefiksere eiendomsnavnet med view
.class
attributtet til span-taggen har blandede dynamiske og statiske klasser tildelt. Alt foran en :
blir en statisk klasse, mens full:glyphicon-star:glyphicon-star-empty
notasjon er som en ternær operatør i JavaScript: hvis hele eiendommen er sann, bør første klasse tildeles; hvis ikke, den andre.setRating
handling bør avfyres - men Ember vil slå det opp på utsikten, ikke ruten eller kontrolleren, som i tilfelle å lage en ny artist.Handlingen er således definert på utsikten:
App.StarRating = Ember.View.extend({ (...) actions: { setRating: function() { var newRating = $(event.target).data('rating'); this.set('rating', newRating); } } });
Vi får vurderingen fra rating
dataattributtet vi tildelte i malen, og satte det som rating
for sangen. Vær oppmerksom på at den nye vurderingen ikke er vedvarende på baksiden. Det ville ikke være vanskelig å implementere denne funksjonaliteten basert på hvordan vi opprettet en kunstner og er igjen som en øvelse for den motiverte leseren.
Vi har smakt på flere ingredienser av den nevnte Ember-kaken:
Vakker, ikke sant?
Det er langt mer med Ember enn jeg kunne få plass i dette innlegget alene. Hvis du vil se en screencast-serie om hvordan jeg bygde en noe mer utviklet versjon av applikasjonen ovenfor og / eller lære mer om Ember, kan du registrer deg på adresselisten min for å få artikler eller tips på ukentlig basis.
Jeg håper jeg har fått lysten til å lære mer om Ember.js, og at du går langt utover eksempelapplikasjonen jeg har brukt i dette innlegget. Når du fortsetter å lære om Ember.js, må du sørge for å se på vårt artikkel om Ember Data for å lære hvordan du bruker ember-data-biblioteket . Ha det gøy å bygge!
I slekt: Ember.js og de 8 vanligste feilene som utviklere gjør