În prima parte a seriei, am vorbit despre componente care vă permit să gestionați comportamente diferite folosind fațete și cum Milo gestionează mesajele.
În acest articol, vom examina o altă problemă comună în dezvoltarea aplicațiilor de browser: Conectarea modelelor la vizualizări. Vom descifra unele dintre "magia" care face posibilă legarea datelor în două direcții în Milo și, pentru a încheia lucrurile, vom construi o aplicație pe deplin funcțională To Do în mai puțin de 50 de linii de cod.
Există mai multe mituri despre JavaScript. Mulți dezvoltatori cred că evalul este rău și nu trebuie folosit niciodată. Această convingere îi face pe mulți dezvoltatori să nu poată spune când evalul poate și ar trebui să fie folosit.
Mantrele ca "eval
este rău "nu poate fi decât dăunător atunci când avem de-a face cu ceva care este în esență un instrument. Un instrument este doar "bun" sau "rău" atunci când este dat un context. Nu ai spune că un ciocan este rău, nu-i așa? Depinde într-adevăr cum îl folosiți. Când este folosit cu un cui și cu unele mobilier, "ciocanul este bun". Când este folosit pentru a-ți face pâinea, "ciocanul este rău".
În timp ce cu siguranță suntem de acord eval
are limitări (de exemplu performanța) și riscuri (mai ales dacă evaluează codul introdus de utilizator), există destul de multe situații când eval este singura modalitate de a atinge funcționalitatea dorită.
De exemplu, multe motoare templante folosesc eval
în sfera de aplicare a cu operator (un alt mare nu-nu în rândul dezvoltatorilor) pentru a compila șabloane pentru funcții JavaScript.
Când ne gândim ce ne-am dorit din modelele noastre, am luat în considerare câteva abordări. Unul a fost să aibă modele superficiale, cum ar fi Backbone-ul, cu mesajele emise în urma modificărilor modelului. În timp ce sunt ușor de implementat, aceste modele ar avea o utilitate limitată - cele mai multe modele de viață reală sunt profunde.
Am considerat utilizarea obiectelor simple JavaScript cu Object.observe
API (care ar elimina necesitatea implementării oricăror modele). În timp ce aplicația noastră era necesară numai pentru a lucra cu Chrome, Object.observe
abia în ultima vreme a devenit activată în mod implicit - anterior, a fost necesară activarea semnului Chrome, ceea ce ar fi făcut dificil implementarea și suportul.
Am vrut modele pe care le putem conecta la vizualizări, dar în așa fel încât să putem schimba structura de vizualizare fără a schimba o singură linie de cod, fără a schimba structura modelului și fără a trebui să gestionăm în mod explicit conversia modelului de vizualizare la Model de date.
Am vrut, de asemenea, să putem conecta modele între ele (consultați programarea reactivă) și să ne abonați la modificările modelului. Uneltele cu unghi implementează prin compararea stărilor modelelor și acest lucru devine foarte ineficient cu modele mari și profunde.
După o discuție, am decis că vom implementa clasa noastră de modele care ar susține un API get / set simplu pentru a le manipula și care ar permite abonarea la schimbări în cadrul acestora:
var m = model nou; m ( 'info.name ') set (' unghiular'.); console.log (m ( 'info') get ().); // jurnale: name: 'angular' m.on ('. info.name', onNameChange); funcția onNameChange (msg, date) console.log ('Nume schimbat de la', data.oldValue, 'to', data.newValue); m ('. info.name'). set ('milo'); // logs: Numele a fost schimbat de la unghiular la milo console.log (m.get ()); // loguri: info: nume: 'milo' console.log (m ('.info'). get ()); // jurnale: name: 'milo'
Acest API arată similar cu accesul la proprietăți obișnuite și ar trebui să ofere un acces profund în siguranță la proprietăți - când obține
se numește pe căile de proprietate inexistente pe care le întoarce nedefinit
, și atunci când a stabilit
se numește, creează arborele obiect / array lipsă după cum este necesar.
Acest API a fost creat înainte de a fi implementat și principala necunoscută cu care ne-am confruntat a fost cum să creăm obiecte care erau și funcții care să poată fi apelate. Se pare că pentru a crea un constructor care returnează obiecte care pot fi chemați, trebuie să returnați această funcție de la constructor și să-i setați prototipul pentru ao face o instanță a Model
clasă în același timp:
funcția Model (date) // ModelPath ar trebui să returneze un obiect ModelPath // cu metode pentru a obține / seta proprietățile modelului, // să se aboneze la modificările proprietății etc. var model = funcția modelPath (calea) returnează ModelPath nou cale); model .__ proto__ = Model.prototype; model._data = date; model._messenger = noul Messenger (model, Messenger.defaultMethods); model de returnare; Model.prototype .__ proto__ = Model .__ proto__;
In timp ce __proto__
proprietatea obiectului este de obicei mai bine evitată, este totuși singura modalitate de a schimba prototipul instanței obiectului și prototipul constructorului.
Cazul lui ModelPath
care ar trebui returnat atunci când modelul este chemat (de ex. m ( 'info.name')
de mai sus) a prezentat o altă provocare de implementare. ModelPath
instanțele ar trebui să aibă metode care să stabilească corect proprietățile modelelor trimise modelului atunci când a fost apelat (.info.name
în acest caz). Am considerat că le punem în aplicare prin simpla analiză a proprietăților transmise ca șiruri ori de câte ori acele proprietăți sunt accesate, dar am realizat că ar fi condus la performanțe ineficiente.
În schimb, am decis să le punem în aplicare astfel încât să m ( 'info.name')
, de exemplu, returnează un obiect (o instanță de ModelPath
"Clasa") care are toate metodele accesoriu (obține
, a stabilit
, del
și lipitură
) sintetizat ca cod JavaScript și transformat în funcții JavaScript folosind eval
.
De asemenea, am realizat toate aceste metode sintetizate în cache, odată ce ați utilizat orice model .info.name
toate metodele accessor pentru această "cale de proprietate" sunt stocate în cache și pot fi reutilizate pentru orice alt model.
Prima implementare a metodei get a arătat astfel:
funcția synthesizeGetter (cale, parsedPath) var getter; var getterCode = 'getter = valoarea funcției ()' + '\ n var m =' + modelAccessPrefix + '; \ n return "; var modelDataProperty = 'm'; pentru (var i = 0, count = parsedPath.length-1; i < count; i++) modelDataProperty += parsedPath[i].property; getterCode += modelDataProperty + ' && '; getterCode += modelDataProperty + parsedPath[count].property + ';\n ;'; try eval(getterCode); catch (e) throw ModelError('ModelPath getter error; path: ' + path + ', code: ' + getterCode); return getter;
Cu exceptia a stabilit
metoda părea mult mai gravă și a fost foarte greu de urmat, citit și menținut, deoarece codul metodei create a fost puternic intercalat cu codul care a generat metoda. Din acest motiv, am trecut la utilizarea motorului template-ului doT pentru a genera codul pentru metodele accessor.
Acesta a fost cel care a obținut după ce a trecut la utilizarea șabloanelor:
var dotDef = modelAccessPrefix: 'this._model._data',; var getterTemplate = 'metoda = valoarea funcției () \ var m = def.modelAccessPrefix; \ var modelDataProperty = "m"; \ return \ pentru (var i = 0, count = it.parsedPath.length-1; \ i < count; i++) \ modelDataProperty+=it.parsedPath[i].property; \ =modelDataProperty && \ \ =modelDataProperty=it.parsedPath[count].property; \ '; var getterSynthesizer = dot.compile(getterTemplate, dotDef); function synthesizeMethod(synthesizer, path, parsedPath) var method , methodCode = synthesizer( parsedPath: parsedPath ); try eval(methodCode); catch (e) throw Error('ModelPath method compilation error; path: ' + path + ', code: ' + methodCode); return method; function synthesizeGetter(path, parsedPath) return synthesizeMethod(getterSynthesizer, path, parsedPath);
Aceasta sa dovedit a fi o abordare bună. Ne-a permis să facem codul pentru toate metodele accesoriale pe care le avem (obține
, a stabilit
, del
și lipitură
) foarte modulare și întreținute.
Modelul API pe care l-am dezvoltat sa dovedit a fi destul de utilizabil și performant. Ea a evoluat pentru a sustine sintaxa elementelor matrice, lipitură
metoda pentru matrice (și metode derivate, cum ar fi Apăsați
, pop
, etc.) și interpolarea accesului la proprietate / element.
Acesta din urmă a fost introdus pentru a evita sintetizarea metodelor accessor (care este o operațiune mult mai lentă care accesează proprietatea sau elementul) atunci când singurul lucru care se schimbă este un anumit indice de proprietate sau element. S-ar întâmpla dacă elementele matrice din interiorul modelului trebuie să fie actualizate în buclă.
Luați în considerare acest exemplu:
pentru (var i = 0; i < 100; i++) var mPath = m('.list[' + i + '].name'); var name = mPath.get(); mPath.set(capitalize(name));
În fiecare iterație, a ModelPath
instanța este creată pentru a accesa și a actualiza proprietatea de nume a elementului matrice din model. Toate instanțele au căi de proprietate diferite și vor necesita sintetizarea a patru metode accesor pentru fiecare dintre cele 100 de elemente care utilizează eval
. Va fi o operație considerabil lentă.
Cu interpolarea accesului proprietății, a doua linie din acest exemplu poate fi schimbată la:
var mPath = m ("lista [$ 1] .name", i);
Nu numai că arată mai ușor de citit, este mult mai rapid. În timp ce încă mai creăm 100 ModelPath
instanțe în această buclă, toți vor împărtăși aceleași metode accesoriu, așa că în loc de 400 vom sintetiza doar patru metode.
Puteți să estimați diferența de performanță dintre aceste mostre.
Milo a implementat programarea reactivă folosind modele observabile care emite notificări asupra lor ori de câte ori oricare dintre proprietățile lor se schimbă. Acest lucru ne-a permis să implementăm conexiuni de date reactive utilizând următorul API:
var conector = minder (m1, '<<<->>> ', m2 (' info ')); // creează conexiune reactivă bidirecțională // între modelul m1 și proprietatea ".info" a modelului m2 // cu adâncimea de 2 (proprietățile și sub-proprietățile // ale modelelor sunt conectate).
După cum puteți vedea din linia de mai sus, ModelPath
întors de m2 ( 'info')
ar trebui să aibă același API ca modelul, ceea ce înseamnă că are același API de mesagerie ca model și este, de asemenea, o funcție:
var mPath = m ("info"); mPath ('.name') set ("); // seta poperty '.info.name' în m mPath.on ('.name', onNameChange); // same as m ('. info.name' .on (", onNameChange) // la fel ca m.on ('. info.name', onNameChange);
În mod similar, putem conecta modele la vederi. Componentele (vezi prima parte a seriei) pot avea o fațetă de date care servește ca un API pentru a manipula DOM ca și cum ar fi un model. Are același API ca model și poate fi utilizat în conexiuni reactive.
Deci, acest cod, de exemplu, conectează o vizualizare DOM la un model:
var conector = minder (m, '<<<->>> ', comp.data);
Acesta va fi demonstrat mai în detaliu mai jos în aplicația To-Do.
Cum funcționează acest conector? Sub capotă, conectorul subscrie pur și simplu la modificările surselor de date de pe ambele părți ale conexiunii și trece modificările primite de la o sursă de date la o altă sursă de date. O sursă de date poate fi un model, o cale de model, o fațetă de date a componentei sau orice alt obiect care implementează același API de mesagerie ca modelul.
Prima implementare a conectorului a fost destul de simplă:
// ds1 și ds2 - surse de date conectate // modul definește direcția și profunzimea funcției de conectare Conector (ds1, mode, ds2) var parsedMode = mode.match (/ ^ (\<*)\-+(\>*) $ /); _.extend (acest lucru, ds1: ds1, ds2: ds2, modul: modul, adâncimea1: parsedMode [1] .length, depth2: parsedMode [2] .length, isOn: false); this.on (); _.extendProto (Conector, on: on, off: off); funcția pe () var subscriptionPath = this._subscriptionPath = array nou (this.depth1 || this.depth2) .join ('*'); var self = aceasta; dacă (this.depth1) linkDataSource ('_ link1', '_link2', this.ds1, this.ds2, subscriptionPath); dacă (this.depth2) linkDataSource ('_ link2', '_link1', this.ds2, this.ds1, subscriptionPath); this.isOn = true; funcția linkDataSource (linkName, stopLink, linkToDS, linkedDS, subscriptionPath) var onData = funcția onData (cale, date) // previne loopul mesajului fără sfârșit // pentru conexiunile bidirecționale dacă (onData .__ stopLink) var dsPath = linkToDS.path (calea); dacă (dsPath) self [stopLink] .__ stopLink = adevărat; dsPath.set (data.newValue); ștergeți auto [stopLink] .__ stopLink; linkedDS.on (abonamentPath, onData); auto [linkName] = onData; return onData; funcția este dezactivată () var self = aceasta; unlinkDataSource (this.ds1, '_link2'); unlinkDataSource (acest.ds2, '_link1'); this.isOn = false; funcția unlinkDataSource (linkedDS, linkName) if (auto [linkName]) linkedDS.off (auto._subscriptionPath, self [linkName]); ștergeți auto [linkName];
Până acum conexiunile reactive în milo au evoluat substanțial - pot schimba structurile de date, pot schimba datele în sine și pot efectua și validări ale datelor. Acest lucru ne-a permis să creăm un generator de formate UI / formate foarte puternice pe care intenționăm să le facem și sursei deschise.
Mulți dintre dvs. vor fi conștienți de proiectul TodoMVC: o colecție de aplicații de aplicații To-Do realizate utilizând o varietate de cadre MV * diferite. Aplicația To-Do este un test perfect al oricărui cadru deoarece este destul de simplu de construit și comparat, însă necesită o gamă destul de largă de funcționalități, inclusiv operațiile CRUD (creați, citiți, actualizați și ștergeți), interacțiunea DOM și vizualizarea / modelul obligatoriu doar pentru a numi câteva.
În diferite etape ale dezvoltării lui Milo, am încercat să construim aplicații simple pentru a face-o, și fără îndoială, a subliniat bug-uri cadru sau deficiențe. Chiar și adânc în proiectul nostru principal, când Milo a fost folosit pentru a sprijini o aplicație mult mai complexă, am găsit mici bug-uri în acest fel. Până în prezent, cadrul acoperă cele mai multe zone necesare dezvoltării aplicațiilor web și găsim codul necesar pentru a construi aplicația To-Do, care este destul de succintă și declarativă.
În primul rând, avem marcajul HTML. Este o placă de bază standard HTML cu un stil de redactare pentru a gestiona elementele verificate. În organism avem un ml-bind
atributul de a declara lista Do-Do, și aceasta este doar o componentă simplă cu listă
fațeta adăugată. Dacă vrem să avem mai multe liste, ar trebui să definim o clasă componentă pentru această listă.
În interiorul listei este elementul nostru de eșantion, care a fost declarat folosind un obicei A face
clasă. În timp ce declararea unei clase nu este necesară, face gestionarea copiilor componentei mult mai simplă și modulară.
Pentru a-Do
Pentru ca noi să fugim milo.binder ()
acum, va trebui mai întâi să definim A face
clasă. Această clasă va trebui să aibă articol
facet, și va fi, în principiu, responsabil pentru gestionarea butonului de ștergere și caseta de selectare care se găsește pe fiecare A face
.
Înainte ca o componentă să poată funcționa asupra copiilor săi, trebuie să aștepte în primul rând childrenbound
eveniment să fie concediat pe el. Pentru mai multe informații despre ciclul de viață al componentelor, consultați documentația (link către documentele componente).
// Crearea unei noi clase de componente fatetate cu fațeta "element". // Aceasta ar fi de obicei definită în fișierul propriu. // Notă: Fata elementului va 'necesita' în // 'container', 'data' și 'dom' facets var Todo = _.createSubclass (milo.Component, 'Todo'); milo.registry.components.add (Todo); // Adăugarea propriei noastre metode init custom _.extendProto (Todo, init: Todo $ init); funcția Todo $ init () // Apelarea metodei inițiale init. milo.Component.prototype.init.apply (acest lucru, argumente); // Ascultarea pentru "childrenbound", care a fost tras după liant / a terminat cu toți copiii acestei componente. this.on ('childrenbound', function () // Avem obiectul (componentele copilului trăiesc aici) var scope = this.container.scope; // Și configurați două abonamente, unul la datele casetei de selectare // Sintaxa abonamentului permite ca contextul să fie trecut de scope.checked.data.on (", abonat: checkTodo, context: this); // și unul la evenimentul" click "al butonului de ștergere scope.deleteBtn.events.on ("click", subscriber: removeTodo, context: this);); // Cand se schimba caseta de validare, vom seta clasa functiei Todo checkTodo (cale, date) this.el.classList.toggle ('todo-item-checked', data.newValue); // // Pentru a elimina elementul, vom folosi metoda 'removeItem' a functiei facet 'item' removeTodo (eventType, event) this.item.removeItem ;
Acum, că avem această configurație, putem apela liantul să atașeze componentele elementelor DOM, să creeze un nou model cu conexiune bidirecțională la listă prin intermediul feței sale de date.
// funcția Milo ready, funcționează ca și funcția de gata a lui jQuery. milo (function () // Apel liant pe documentul // Se atașează componentelor elementelor DOM cu ajutorul atributului ml-bind var scope = milo.binder (); // Obțineți acces la componentele noastre prin intermediul obiectului de domeniu var todos = scope.todos // Lista Todos, newTodo = scope.newTodo // Introducere nou todo, addBtn = scope.addBtn // Buton Add, modelView = scope.modelView; // Unde tiparim modelul // Configurarea modelului nostru, aceasta va țineți gama de todos var m = new milo.Model; // Acest abonament va arăta conținutul modelului // în orice moment sub todos m.on (/.*/, funcția showModel (msg, data) modelView.data.set (JSON.stringify (m.get ()));)); // Creați o legătură profundă în două direcții între modelul nostru și aspectul datelor listă todos. de asemenea, să fie o cale), // restul definește adâncimea conexiunii - 2 nivele în acest caz, pentru a include // proprietățile elementelor matrice milo.minder (m, '<<<->>> ', todos.data); // Abonamentul la evenimentul de clic al butonului add addBtn.events.on ('click', addTodo); // Funcționarea clicului pentru funcția butonului add addTodo () // Pachetăm intrarea 'newTodo' ca obiect // Textul proprietății corespunde marcării elementului. var itemData = text: newTodo.data.get (); // Aplicăm aceste date în model. // Vizualizarea va fi actualizată automat! m.push (itemData); // Și, în final, setați din nou intrarea pentru a șterge. newTodo.data.set ("););
Această probă este disponibilă în jsfiddle.
Proba de eșantion este foarte simplă și arată o parte foarte mică din puterea minunată a lui Milo. Milo are multe caracteristici care nu sunt acoperite de acest articol și de articolele anterioare, inclusiv drag and drop, utilitare locale pentru stocarea locală, http și websockets, utilitare DOM avansate etc..
În prezent, Milo are puterile noului CMS al dailymail.co.uk (acest CMS are zeci de mii de cod javascript frontal și este folosit pentru a crea mai mult de 500 de articole în fiecare zi).
Milo este open source și încă într-o fază beta, deci este un moment bun să experimentăm cu ea și poate chiar să contribuiți. Ne-ar plăcea feedback-ul.
Rețineți că acest articol a fost scris atât de Jason Green, cât și de Evgeny Poberezkin.