Construirea unui cadru de la zero nu este ceva ce ne-am propus în mod special. Ar trebui să fii nebun, nu? Cu multitudinea de cadre JavaScript aflate acolo, ce motivație ar fi posibil să avem pentru a ne rula pe noi?
Începusem să căutăm un cadru pentru construirea noului sistem de gestionare a conținutului pentru site-ul The Daily Mail. Obiectivul principal a fost acela de a face procesul de editare mult mai interactiv, cu toate elementele unui articol (imagini, încorporări, casete de apel etc.) fiind draggable, modular și self-managing.
Toate cadrele pe care le-am putea pune mâna au fost proiectate pentru mai mult sau mai puțin UI statice definite de dezvoltatori. A trebuit să facem un articol atât cu text editabil, cât și cu elemente de interfață UI rendered dinamic.
Coloana vertebrală a fost prea mică. Ea nu a făcut decât să furnizeze structuri de bază și mesaje. Ar fi trebuit să construim o mulțime de abstracțiuni deasupra fundației Backbone, deci am hotărât să ne construim mai degrabă această fundație.
AngularJS a devenit cadrul nostru de alegere pentru construirea de aplicații de browser de dimensiuni mici și medii, care au UI relativ statice. Din păcate, AngularJS este foarte mult o cutie neagră - nu expune niciun API convenabil pentru a extinde și manipula obiectele pe care le creați - directive, controlori, servicii. De asemenea, în timp ce AngularJS furnizează conexiuni reactive între vizualizări și expresii de domeniu, nu permite definirea conexiunilor reactive între modele, astfel încât orice aplicație de dimensiune medie devine foarte asemănătoare cu o aplicație jQuery cu spaghetele ascultătorilor de evenimente și callback-uri, cu singura diferență că în loc de ascultători de evenimente, o aplicație unghiulară are observatori și în loc de a manipula DOM vă manipulați domeniile.
Ceea ce am dorit mereu era un cadru care să permită;
Nu am putut găsi ceea ce aveam nevoie în soluțiile existente, așa că am început să dezvoltăm Milo în paralel cu aplicația care o folosește.
Milo a fost ales drept nume datorită lui Milo Minderbinder, un profet al războiului Prinde 22 de Joseph Heller. După ce a pornit de la gestionarea operațiunilor de mizerie, le-a extins într-o întreprindere profitabilă, care le-a conectat pe toți cu totul, și că Milo și toți ceilalți au "o parte".
Cadrul Milo are un liant de module, care leagă elementele DOM de componente (prin intermediul unor elemente speciale ml-bind
atribut) și modulul de monitorizare a modulelor care permite stabilirea conexiunilor reactive în direct între diferite surse de date (modelul și datele de date ale componentelor sunt astfel de surse de date).
În mod coincidență, Milo poate fi citit ca un acronim al MaIL Online și fără mediul de lucru unic la Mail Online, nu am fi reușit niciodată să îl construim.
Vizualizările din Milo sunt gestionate de componente, care sunt în esență instanțe de clase JavaScript, responsabile de gestionarea unui element DOM. Multe cadre utilizează componente ca un concept pentru a gestiona elementele UI, dar cea mai evidentă care vine în minte este Ext JS. Am lucrat extensiv cu Ext JS (aplicația moștenită pe care o înlocuim a fost construită cu ea) și a vrut să evite ceea ce am considerat a fi două dezavantaje ale abordării sale.
Primul este că Ext JS nu vă ușurează gestionarea marcajului. Singura modalitate de a construi un UI este de a pune împreună ierarhiile imbricate ale configurațiilor componentelor. Acest lucru conduce la marcarea inutil de complexă și preia controlul din mâinile dezvoltatorului. Aveam nevoie de o metodă de creare a componentelor în linie, în propria noastră marcaj HTML manual. Aici intră binderul.
Binder scanează marcajul nostru căutând ml-bind
atribut astfel încât să poată instanțiza componentele și le leagă de element. Atributul conține informații despre componente; aceasta poate include clasa componentei, fațetele și trebuie să includă numele componentei.
Componenta noastră milo
Vom vorbi despre fațete într-un minut, dar pentru moment să ne uităm la modul în care putem lua această valoare de atribut și extragem configurația de la ea folosind o expresie regulată.
var bindAttrRegex = / ^ ([^: \ [\]] *) (?: \ [(^ ^: \ [ ; var rezultat = valoare.match (bindAttrRegex); // rezultatul este o matrice cu // result [0] = 'ComponentClass [facet1, facet2]: componentName'; // rezultat [1] = 'ComponentClass'; // rezultatul [2] = 'facet1, facet2'; // rezultat [3] = 'componentName';
Cu aceste informații, tot ce trebuie să facem este să repetăm tot ml-bind
atribute, extrageți aceste valori și creați instanțe pentru a gestiona fiecare element.
var bindAttrRegex = / ^ ([^: \ [\]] *) (?: \ [(^ ^: \ [ ; funcție de legare (callback) var scope = ; // primim toate elementele cu atributul ml-bind var els = document.querySelectorAll ('[ml-bind]'); Array.prototype.forEach.call (els, function (el) var attrText = el.getAttribute ('ml-bind'); var rezultat = attrText.match (bindAttrRegex); var className = result [1] 'var' rezultatul [2] .split (','); var compName = rezultate [3]; // presupunând că avem un obiect de registru al tuturor claselor noastre var comp = new classRegistry [className] .addFacets (fațete); comp.name = compName; domeniul [compName] = comp; // păstrăm o referință la componenta de pe elementul .___ milo_component = comp;); callback (domeniul de aplicare); binder (funcție (domeniu) console.log (scope););
Deci, cu doar un pic de regex și unele traversal DOM, vă puteți crea propriul mini-cadru cu sintaxa personalizată pentru a se potrivi logicii și contextului dvs. de afaceri special. În codul foarte mic, am instalat o arhitectură care permite componente modulare, auto-gestionate, care pot fi utilizate oricum doriți. Putem crea o sintaxă convenabilă și declarativă pentru instanțierea și configurarea componentelor în HTML, dar spre deosebire de unghiul, putem gestiona aceste componente, totuși ne place.
Cel de-al doilea lucru care nu ne-a plăcut despre Ext JS a fost acela că are o ierarhie de clasă foarte abruptă și rigidă, ceea ce ar fi îngreunat organizarea claselor componente. Am încercat să scriem o listă a tuturor comportamentelor pe care le-ar putea avea orice componentă dintr-un articol. De exemplu, o componentă ar putea fi editabilă, ar putea fi ascultată pentru evenimente, ar putea fi o țintă de cădere sau poate fi trasată în sine. Acestea sunt doar câteva dintre comportamentele necesare. O listă preliminară pe care am scris-o avea aproximativ 15 tipuri diferite de funcționalități care ar putea fi necesare pentru orice componentă particulară.
Încercarea de a organiza aceste comportamente într-un fel de structură ierarhică ar fi fost nu numai o durere de cap majoră, ci și o limitare foarte limitată pentru a schimba funcționalitatea oricărei clase de componente (ceea ce am făcut foarte mult). Am decis să implementăm un model de design mai flexibil, orientat spre obiect.
Am citit un proiect de responsabilitate bazat pe responsabilitate, care, contrar modelului mai comun de definire a comportamentului unei clase, împreună cu datele pe care le deține, este mai preocupat de acțiunile pe care un obiect este responsabil. Acest lucru ne-a adaptat bine, deoarece avem de-a face cu un model de date complex și imprevizibil, iar această abordare ne-ar permite să lăsăm implementarea acestor detalii mai târziu.
Cel mai important lucru pe care l-am luat de la RDD a fost conceptul de roluri. Un rol este un set de responsabilități conexe. În cazul proiectului nostru, am identificat roluri cum ar fi editare, tragere, zonă de lansare, selectabile sau evenimente printre multe altele. Dar cum reprezentați aceste roluri în cod? Pentru asta am împrumutat din modelul de decorator.
Modelul de decorator permite ca un comportament să fie adăugat unui obiect individual, fie în mod static, fie dinamic, fără a afecta comportamentul altor obiecte din aceeași clasă. Acum, în timp ce manipularea în timp a comportamentului de clasă nu a fost deosebit de necesară în acest proiect, am fost foarte interesați de tipul de încapsulare pe care această idee o oferă. Implementarea lui Milo este un fel de hibrid care implică obiecte numite fațete, fiind atașat ca proprietăți instanței componente. Fata face o referire la componentă, este "proprietar" și un obiect de configurare, care ne permite să personalizăm fațete pentru fiecare clasă de componente.
Vă puteți gândi la fațete ca amestecuri avansate, configurabile care își obțin propriul spațiu de nume pe obiectul proprietarului lor și chiar pe propriul lor domeniu init
, care trebuie suprascrisă de subclasa de fațet.
funcția Facet (proprietar, config) this.name = this.constructor.name.toLowerCase (); thisowner = proprietar; this.config = config || ; acest lucru se aplică (acest lucru, argumente); Facet.prototype.init = funcția Facet $ init () ;
Deci, putem subclasa acest simplu Faţetă
clasă și să creeze fațete specifice pentru fiecare tip de comportament pe care îl dorim. Milo vine pre-construit cu o varietate de fațete, cum ar fi DOM
facet, care oferă o colecție de utilități DOM care operează pe elementul componentei proprietarului, și Listă
și Articol
fațete, care lucrează împreună pentru a crea liste de componente repetate.
Aceste fațete sunt apoi reunite prin ceea ce noi am numit a FacetedObject
, care este o clasă abstractă din care moștenesc toate componentele. FacetedObject
are o metodă de clasă numită createFacetedClass
care pur și simplu subclase în sine, și leagă toate fațetele de a fațete
proprietate asupra clasei. În acest fel, atunci când FacetedObject
devine instanțiată, are acces la toate clasele de fațete și le poate repeta pentru a bootstrap componentele.
funcția FacetedObject (facetsOptions / *, alte init args * /) facetsOptions = facetsOptions? _.clone (facetsOptions): ; var thisClass = acest.constructor, facets = ; dacă (! thisClass.prototype.facets) aruncă o eroare nouă ("Nu sunt definite fețe"); _.eachKey (this.facets, instantiateFacet, this, true); Object.defineProperties (aceasta, fațete); dacă (this.init) this.init.apply (acest lucru, argumente); funcția instantiateFacet (facetClass, fct) var facetOpts = facetsOptions [fct]; ștergeți fețele Opțiuni [fct]; fațete [fct] = enumerable: false, valoare: new facetClass (this facetOpts); FacetedObject.createFacetedClass = funcție (nume, fețeClasses) var FacetedClass = _.createSubclass (acest nume, adevărat); _.extendProto (FacetedClass, facets: facetsClasses); returnează FacetedClass; ;
În Milo, am abstracționat puțin mai mult, creând o bază component
clasă cu potrivire createComponentClass
clasa, dar principiul de bază este același. Cu comportamentele cheie fiind gestionate de fațete configurabile, putem crea mai multe clase de componente diferite într-un stil declarativ fără a fi nevoie să scriem prea mult cod personalizat. Iată un exemplu care folosește câteva dintre fațetele care apar cu Milo.
var: message: 'click': onPanelClick, trageți: messages: cls: 'my-panel' ..., aruncați: messages: ..., container: undefined);
Aici am creat o clasă de componente numită Panou
, care are acces la metodele de utilitate DOM, își va seta automat clasa CSS init
, acesta poate asculta evenimentele DOM și va configura un handler pentru clicuri init
, acesta poate fi tras în jurul valorii, și, de asemenea, acționează ca o țintă de picătură. Ultima fațetă acolo, recipient
se asigură că această componentă își stabilește propriul scop și poate, de fapt, să aibă componente pentru copii.
Am discutat pentru o vreme dacă toate componentele atașate documentului ar trebui să formeze o structură plană sau ar trebui să formeze propriul copac, unde copiii sunt accesibili numai de la părintele lor.
Ne-am fi cu siguranță nevoie de domenii pentru anumite situații, dar ar fi putut fi tratate la nivel de implementare, mai degrabă decât la nivel cadru. De exemplu, avem grupuri de imagini care conțin imagini. Ar fi fost simplu pentru aceste grupuri să păstreze o evidență a imaginilor copilului lor fără a fi nevoie de un domeniu generic.
Am decis în cele din urmă să creăm un arbore de dimensiuni al componentelor din document. Având scopuri face mai multe lucruri mai ușoare și ne permite să avem nume mai generice de componente, dar ele trebuie, în mod evident, să fie gestionate. Dacă distrugeți o componentă, trebuie să o eliminați din domeniul său de aplicare. Dacă mutați o componentă, trebuie să fie eliminată de la una și adăugată la alta.
Domeniul de aplicare este un hash special, sau un obiect de hartă, fiecare dintre copii fiind inclus în domeniul de aplicare ca proprietăți ale obiectului. Domeniul de aplicare, în Milo, se găsește pe fațada containerului, care are însă foarte puține funcționalități. Cu toate acestea, obiectul scopului are o varietate de metode de manipulare și iterare, dar pentru a evita conflictele din spațiul de nume, toate aceste metode sunt denumite cu un subliniere la început.
var domeniu = myComponent.container.scope; scope._each (funcția (childComp) // iterați fiecare componentă copil); // accesați o componentă specifică pe domeniul var testComp = scope.testComp; // a obține numărul total de componente copil var total = scope._length (); // adăugați o nouă componentă din domeniul de aplicare scope._add (newComp);
Am dorit să avem o legătură slabă între componente, deci am decis să avem funcționalitatea de mesagerie atașată tuturor componentelor și fațadelor.
Prima implementare a mesagerului era doar o colecție de metode care gestionau rețelele de abonați. Atât metodele, cât și matricea au fost amestecate chiar în obiectul care a implementat mesajele.
O versiune simplificată a implementării primului mesager arată astfel:
var messengerMixin = initMessenger: initMessenger, pe: on, off: off, postMessage: postMessage; funcția initMessenger () this._subscribers = ; funcția pe (mesaj, abonat) var msgSubscribers = this._subscribers [message] = this._subscribers [message] || []; dacă (msgSubscribers.indexOf (abonat) == -1) msgSubscribers.push (abonat); funcția dezactivată (mesaj, abonat) var msgSubscribers = this._subscribers [message]; dacă (msgSubscribers) if (abonat) _.spliceItem (msgSubscribers, abonat); altceva ștergeți acest mesaj. postMessage (mesaj, date) var msgSubscribers = this._subscribers [message]; dacă (msgSubscribers) msgSubscribers.forEach (funcție (abonat) subscriber.call (acest mesaj, date););
Orice obiect care folosește acest mix-in poate avea mesaje emise pe el (de către obiect în sine sau prin orice alt cod) cu postMessage
metoda și abonamentele la acest cod pot fi activate și dezactivate cu metode care au aceleași nume.
În prezent, mesagerii au evoluat substanțial pentru a permite:
Evenimente
fațeta o folosește pentru a expune evenimentele DOM prin intermediul mesagerului Milo. Această funcționalitate este implementată printr-o clasă separată MessageSource
și subclasele sale.Date
facet îl folosește pentru a traduce modificările și intrările evenimentelor DOM la evenimentele de schimbare a datelor (a se vedea Modelele de mai jos). Această funcție este implementată printr-o clasă separată MessengerAPI și subclasele sale.component.on ('stateready', abonat: func, context: context);
o singura data
metodăpostMessage
(am considerat numărul variabil de argumente în postMessage
, dar am dorit o API de mesagerie mai consistentă decât am avea cu argumentele variabile)Principala greșeală de proiectare pe care am făcut-o în timpul dezvoltării mesagerului a fost că toate mesajele au fost expediate în mod sincron. Deoarece JavaScript este un singur thread, secvențe lungi de mesaje cu operații complexe care se efectuează ar bloca destul de ușor UI. Schimbarea lui Milo pentru a face trimiterea mesajului asincron a fost ușoară (toți abonații sunt chemați pe propriile lor blocuri de execuție folosind setTimeout (abonat, 0)
, schimbarea restului cadrului și a aplicației a fost mai dificilă - în timp ce majoritatea mesajelor pot fi expediate în mod asincron, există multe care trebuie încă expediate în mod sincron (multe evenimente DOM care au date în ele sau în locurile unde preventDefault
se numește). În mod prestabilit, mesajele sunt expediate în mod asincron și există o modalitate de a le face sincronizate fie atunci când mesajul este trimis:
component.postMessageSync ("mesajul meu", date);
sau când este creată abonamentul:
component.onSync ("mesajul meu", funcția (msg, data) // ...);
O altă decizie de proiectare pe care am făcut-o a fost modul în care am expus metodele de mesager pe obiectele care le foloseau. Inițial, metodele au fost pur și simplu amestecate în obiect, dar nu ne-a plăcut că toate metodele sunt expuse și nu am putea avea mesageri independenți. Astfel, mesagerii au fost re-implementați ca o clasă separată bazată pe o clasă abstractă Mixin.
Clasa Mixin permite expunerea metodelor unei clase pe un obiect gazdă în așa fel încât atunci când se apelează metode, contextul va fi mai degrabă Mixin decât obiectul gazdă.
Sa dovedit a fi un mecanism foarte convenabil - putem avea control complet asupra metodelor expuse și a schimba numele după cum este necesar. De asemenea, ne-a permis să avem doi mesageri pe un obiect, care este folosit pentru modele.
În general, mesagerul Milo sa dovedit a fi o piesă foarte solidă de software care poate fi utilizată singură, atât în browser, cât și în Node.js. A fost întărită de utilizarea în sistemul nostru de management al conținutului de producție, care are zeci de mii de linii de cod.
În următorul articol, vom analiza probabil cea mai utilă și mai complexă parte a lui Milo. Modelele Milo nu permit doar acces sigur, adânc la proprietăți, dar și abonament la evenimente la orice nivel.
Vom explora, de asemenea, punerea în aplicare a minder, și modul în care folosim obiectele conectorilor pentru a face legătura unică sau bidirecțională a surselor de date.
Rețineți că acest articol a fost scris atât de Jason Green, cât și de Evgeny Poberezkin.