Ember.js е цялостна рамка за изграждане на сложни клиентски приложения. Един от нейните принципи е „конвенцията за конфигурацията“ и убеждението, че има много голяма част от разработката, обща за повечето уеб приложения, и по този начин един най-добър начин за решаване на повечето от тези ежедневни предизвикателства. Намирането на правилната абстракция и обхващането на всички случаи обаче отнема време и принос от цялата общност. Тъй като разсъжденията продължават, по-добре е да отделите време, за да получите правилно решението на основния проблем, и след това да го изпечете в рамката, вместо да повдигаме ръце и да оставяме всички да се оправят сами, когато трябва да намерят решение.
Ember.js непрекъснато се развива още по-лесно развитие . Но, както при всяка усъвършенствана рамка, все още има клопки, в които могат да попаднат разработчиците на Ember. Със следващия пост се надявам да предоставя карта, която да ги избегне. Нека скочим направо!
Да приемем, че имаме следните маршрути в нашето приложение:
Router.map(function() { this.route('band', { path: 'bands/:id' }, function() { this.route('songs'); }); });
band
маршрут има динамичен сегмент, id
. Когато приложението се зареди с URL като /bands/24
, 24
се предава на model
кука на съответния маршрут, band
. Куката на модела има ролята на десериализиране на сегмента, за да създаде обект (или масив от обекти), който след това може да се използва в шаблона:
// app/routes/band.js export default Ember.Route.extend({ model: function(params) { return this.store.find('band', params.id); // params.id is '24' } });
Дотук добре. Има обаче други начини за въвеждане на маршрути, освен зареждането на приложението от навигационната лента на браузъра. Един от тях използва link-to
помощник от шаблони. Следващият фрагмент преминава през списък с ленти и създава връзка към съответните им band
маршрути:
{#each bands as } {{link-to band.name 'band' band}} {{/each}}
Последният аргумент за връзка към, band
, е обект, който запълва динамичния сегмент за маршрута и по този начин id
се превръща в id сегмент за маршрута. Капанът, в който попадат много хора, е, че в този случай куката на модела не се извиква, тъй като моделът вече е известен и е предаден. Има смисъл и може да запази заявка към сървъра, но това, разбира се, не е интуитивен. Гениален начин, който трябва да премине, не самият обект, а неговият идентификатор:
{band} {{link-to band.name 'band' band.id}} {{/each}}
Скоро маршрутизиращите компоненти ще дойдат в Ембер, вероятно във версия 2.1 или 2.2 . Когато кацнат, куката на модела винаги ще бъде извикана, независимо как се преминава към маршрут с динамичен сегмент. Прочетете съответния RFC тук .
Маршрутите в Ember.js настройват свойства на контролери, които служат като контекст за съответния шаблон. Тези контролери са единични и следователно всяко състояние, дефинирано върху тях, продължава дори когато контролерът вече не е активен.
Това е нещо, което е много лесно да се пренебрегне и И аз се натъкнах на това . В моя случай имах приложение за музикален каталог с групи и песни. songCreationStarted
флаг на songs
контролер посочи, че потребителят е започнал да създава песен за определена група. Проблемът беше, че ако потребителят след това превключи на друга лента, стойността на songCreationStarted
продължи и изглеждаше, че полузавършената песен е за другата група, което беше объркващо.
Решението е да рестартирате ръчно свойствата на контролера, които не искаме да се бавим. Едно от възможните места за това е setupController
кука на съответния маршрут, който се извиква при всички преходи след afterModel
hook (който, както подсказва името му, идва след model
hook):
// app/routes/band.js export default Ember.Route.extend({ setupController: function(controller, model) { this._super(controller, model); controller.set('songCreationStarted', false); } });
Отново зората на маршрутизируеми компоненти ще реши този проблем, като изобщо ще сложи край на контролерите. Едно от предимствата на маршрутизиращите се компоненти е, че те имат по-последователен жизнен цикъл и винаги се разрушават, когато се отдалечават от маршрутите си. Когато пристигнат, горният проблем ще изчезне.
setupController
Маршрутите в Ember имат шепа куки за жизнен цикъл, за да дефинират специфично поведение на приложението. Вече видяхме model
който се използва за извличане на данни за съответния шаблон и setupController
, за настройка на контролера, контекста на шаблона.
Последният, setupController
, има разумно подразбиране, което присвоява модела, от model
кука като model
свойство на контролера:
// ember-routing/lib/system/route.js setupController(controller, context, transition) { if (controller && (context !== undefined)) { set(controller, 'model', context); } }
(context
е името, използвано от пакета ember-routing
за това, което наричам model
по-горе)
setupController
hook може да бъде заменен за няколко цели, като нулиране на състоянието на контролера (както е в Common Mistake No. 2 по-горе). Ако обаче забравите да извикате родителската реализация, която копирах по-горе в Ember.Route, може да се включите в дълга сесия за надраскване на главата, тъй като контролерът няма да има model
набор от свойства. Затова винаги се обаждайте this._super(controller, model)
:
export default Ember.Route.extend({ setupController: function(controller, model) { this._super(controller, model); // put the custom setup here } });
Както беше посочено по-горе, контролерите и заедно с тях setupController
кука, ще изчезнат скоро, така че тази клопка вече няма да бъде заплаха. Тук обаче има по-голям урок, който трябва да се има предвид при прилагането на предците. init
функция, дефинирана в Ember.Object
, майката на всички обекти в Ember, е друг пример, за който трябва да внимавате.
this.modelFor
с не-родителски маршрутиРутерът Ember разрешава модела за всеки сегмент от маршрута, докато обработва URL адреса. Да приемем, че имаме следните маршрути в нашето приложение:
Router.map({ this.route('bands', function() { this.route('band', { path: ':id' }, function() { this.route('songs'); }); }); });
Даден URL адрес на /bands/24/songs
, model
кука от bands
, bands.band
и след това bands.band.songs
се извикват в този ред. API на маршрута има особено удобен метод, modelFor
, който може да се използва в дъщерни маршрути за извличане на модела от един от родителските маршрути, тъй като този модел със сигурност е разрешен от тази точка.
Например следният код е валиден начин за извличане на лентовия обект в bands.band
маршрут:
// app/routes/bands/band.js export default Ember.Route.extend({ model: function(params) { var bands = this.modelFor('bands'); return bands.filterBy('id', params.id); } });
Често срещана грешка обаче е използването на име на маршрут в modelFor that is не родител на маршрута. Ако маршрутите от горния пример бяха леко променени:
Router.map({ this.route('bands'); this.route('band', { path: 'bands/:id' }, function() { this.route('songs'); }); });
Нашият метод за извличане на лентата, посочена в URL, би се счупил, тъй като bands
route вече не е родител и следователно моделът му не е разрешен.
// app/routes/bands/band.js export default Ember.Route.extend({ model: function(params) { var bands = this.modelFor('bands'); // `bands` is undefined return bands.filterBy('id', params.id); // => error! } });
Решението е да се използва modelFor
само за родителски маршрути и използвайте други средства за извличане на необходимите данни, когато modelFor
не може да се използва, като извличане от магазина.
// app/routes/bands/band.js export default Ember.Route.extend({ model: function(params) { return this.store.find('band', params.id); } });
Вложените компоненти винаги са били една от най-трудните части на Ember за размисъл. С въвеждането на блокиране на параметри в Ember 1.10 , голяма част от тази сложност е облекчена, но в много ситуации все още е сложно да се види с един поглед върху кой компонент ще бъде задействано действие, задействано от детски компонент.
Да приемем, че имаме band-list
компонент, който има band-list-items
и можем да маркираме всяка група като фаворит в списъка.
// app/templates/components/band-list.hbs {band} {{band-list-item band=band faveAction='setAsFavorite'}} {{/each}}
Името на действието, което трябва да се извика, когато потребителят щракне върху бутона, се предава в band-list-item
компонент и се превръща в стойността на неговия faveAction
Имот.
Нека сега видим дефиницията на шаблон и компонент на band-list-item
:
// app/templates/components/band-list-item.hbs {{band.name}} Fave this
// app/components/band-list-item.js export default Ember.Component.extend({ band: null, faveAction: '', actions: { faveBand: { this.sendAction('faveAction', this.get('band')); } } });
Когато потребителят щракне върху бутона „Fave this“, faveBand
действието се задейства, което задейства компонента faveAction
който е предаден в (setAsFavorite
, в горния случай), на неговия родителски компонент , band-list
.
Това отключва много хора, тъй като те очакват действието да бъде задействано по същия начин, както действията от шаблони, управлявани от маршрути, на контролера (и след това да се издигат на активните маршрути). Това, което влошава това е, че не се регистрира съобщение за грешка; родителският компонент просто поглъща грешката.
Общото правило е, че действията се задействат в текущия контекст. В случай на некомпонентни шаблони този контекст е текущият контролер, докато в случай на шаблони на компоненти това е родителският компонент (ако има такъв) или отново текущият контролер, ако компонентът не е вложен.
Така че в горния случай, band-list
компонент ще трябва да задейства повторно действието, получено от band-list-item
за да го изведете до контролера или маршрута.
// app/components/band-list.js export default Ember.Component.extend({ bands: [], favoriteAction: 'setFavoriteBand', actions: { setAsFavorite: function(band) { this.sendAction('favoriteAction', band); } } });
Ако band-list
е дефиниран в bands
шаблон, след това setFavoriteBand
действието ще трябва да се обработва в bands
контролер или bands
маршрут (или един от неговите родителски маршрути).
Можете да си представите, че това става по-сложно, ако има повече нива на влагане (например, като имате fav-button
компонент вътре band-list-item
). Трябва да пробиете дупка през няколко слоя отвътре, за да изведете съобщението си, като дефинирате смислени имена на всяко ниво (setAsFavorite
, favoriteAction
, faveAction
и т.н.)
Това се улеснява от „Подобрени действия RFC“ , който вече е достъпен в главния клон и вероятно ще бъде включен в 1.13.
Тогава горният пример ще бъде опростен до:
// app/templates/components/band-list.hbs {#each bands as } {{band-list-item band=band setFavBand=(action 'setFavoriteBand')}} {{/each}}
// app/templates/components/band-list-item.hbs {{band.name}} Fave this
Изчислените свойства на Ember зависят от други свойства и тази зависимост трябва да бъде изрично дефинирана от разработчика. Да кажем, че имаме isAdmin
свойство, което трябва да е вярно тогава и само ако една от ролите е admin
. Ето как човек може да го напише:
isAdmin: function() { return this.get('roles').contains('admin'); }.property('roles')
С горната дефиниция стойността на isAdmin
се обезсилва само ако roles
array обектът сам се променя, но не и ако елементите се добавят или премахват към съществуващия масив. Съществува специален синтаксис, който определя, че добавянията и премахванията също трябва да предизвикат преизчисление:
isAdmin: function() { return this.get('roles').contains('admin'); }.property('roles.[]')
Нека разширим (сега фиксирания) пример от Common Mistake No. 6 и да създадем потребителски клас в нашето приложение.
var User = Ember.Object.extend({ initRoles: function() { var roles = this.get('roles'); if (!roles) { this.set('roles', []); } }.on('init'), isAdmin: function() { return this.get('roles').contains('admin'); }.property('roles.[]') });
Когато добавим admin
роля на такъв User
, очакваме изненада:
var user = User.create(); user.get('isAdmin'); // => false user.get('roles').push('admin'); user.get('isAdmin'); // => false ?
Проблемът е, че наблюдателите няма да се задействат (и по този начин изчислените свойства няма да се актуализират), ако се използват стоковите Javascript методи. Това може да се промени, ако глобалното приемане на Object.observe
в браузърите се подобрява, но дотогава трябва да използваме набора от методи, които Ember предоставя. В настоящия случай, pushObject
е еквивалент на push
, подходящ за наблюдатели:
user.get('roles').pushObject('admin'); user.get('isAdmin'); // => true, finally!
Представете си, че имаме star-rating
компонент, който показва рейтинга на артикула и позволява задаването на рейтинга на артикула. Оценката може да бъде за песен, книга или дрибъл умение на футболист.
Бихте го използвали по този начин във вашия шаблон:
{song} {{star-rating item=song rating=song.rating}} {{/each}}
Нека приемем още, че компонентът показва звезди, по една пълна звезда за всяка точка и празни звезди след това, до максимален рейтинг. Когато щракнете върху звезда, a set
действието се задейства върху контролера и трябва да се тълкува като потребител, който иска да актуализира рейтинга. Можем да напишем следния код, за да постигнем това:
// app/components/star-rating.js export default Ember.Component.extend({ item: null, rating: 0, (...) actions: { set: function(newRating) { var item = this.get('item'); item.set('rating', newRating); return item.save(); } } });
Това би свършило работата, но има няколко проблема с нея. Първо, приема се, че предаденият елемент има rating
и затова не можем да използваме този компонент за управление на умението на Лео Меси да дриблира (където това свойство може да се нарече score
).
Второ, той мутира рейтинга на елемента в компонента. Това води до сценарии, при които е трудно да се разбере защо дадено свойство се променя. Представете си, че имаме друг компонент в същия шаблон, където тази оценка също се използва, например, за изчисляване на средния резултат за футболиста.
Лозунгът за смекчаване на сложността на този сценарий е „Данни надолу, действия нагоре“ (DDAU). Данните трябва да се предават (от маршрут към контролер към компоненти), докато компонентите трябва да използват действия, за да уведомяват контекста си за промените в тези данни. И така, как трябва да се приложи DDAU тук?
Нека добавим име на действие, което трябва да бъде изпратено за актуализиране на рейтинга:
{song} {{star-rating item=song rating=song.rating setAction='updateRating'}} {{/each}}
И след това използвайте това име за изпращане на действието нагоре:
// app/components/star-rating.js export default Ember.Component.extend({ item: null, rating: 0, (...) actions: { set: function(newRating) { var item = this.get('item'); this.sendAction('setAction', { item: this.get('item'), rating: newRating }); } } });
И накрая, действието се обработва нагоре по веригата, от контролера или маршрута и тук се актуализира рейтингът на артикула:
// app/routes/player.js export default Ember.Route.extend({ actions: { updateRating: function(params) { var skill = params.item, rating = params.rating; skill.set('score', rating); return skill.save(); } } });
Когато това се случи, тази промяна се разпространява надолу чрез обвързването, предадено на star-rating
компонент и броят на показаните пълни звезди се променя в резултат на това.
По този начин мутацията не се случва в компонентите и тъй като единствената специфична част на приложението е обработката на действието в маршрута, повторната употреба на компонента не страда.
Бихме могли да използваме същия компонент за футболни умения:
{skill} {{star-rating item=skill rating=skill.score setAction='updateSkill'}} {{/each}}
Важно е да се отбележи, че някои (повечето?) От грешките, които съм виждал, че хората извършват (или съм се ангажирал), включително тези, за които съм писал тук, ще изчезнат или ще бъдат значително смекчени в началото на поредицата 2.x на Ember.js.
Това, което остава, е адресирано от моите предложения по-горе, така че след като се развиете в Ember 2.x, няма да имате извинение да правите повече грешки! Ако искате тази статия като pdf, насочете се към моя блог и щракнете върху връзката в долната част на публикацията.
Дойдох във фронталния свят с Ember.js преди две години и аз съм тук, за да остана. Бях толкова ентусиазиран от Ембър, че започнах интензивно да блогвам както в публикации за гости, така и в собствения си блог, както и да представям на конференции. Дори написах книга, Рок енд рол с Ember.js , за всеки, който иска да научи Ember. Можете да изтеглите примерна глава тук .