Nella prima parte della serie, abbiamo parlato di componenti che consentono di gestire diversi comportamenti utilizzando le sfaccettature e di come Milo gestisce la messaggistica.
In questo articolo, esamineremo un altro problema comune nello sviluppo di applicazioni browser: la connessione di modelli a viste. Scopriremo una parte della "magia" che rende possibile l'associazione dei dati bidirezionale in Milo e, per concludere le cose, costruiremo un'applicazione To Do completamente funzionale in meno di 50 righe di codice.
Ci sono diversi miti su JavaScript. Molti sviluppatori credono che eval sia malvagio e non dovrebbe mai essere usato. Questa convinzione induce molti sviluppatori a non essere in grado di dire quando può e può essere utilizzato.
Mantra come "eval
è il male "può essere dannoso solo quando abbiamo a che fare con qualcosa che è essenzialmente uno strumento. Uno strumento è solo "buono" o "cattivo" quando viene fornito un contesto. Non diresti che un martello è malvagio, giusto? Dipende davvero da come lo usi. Se usato con un chiodo e alcuni mobili, "il martello è buono". Se usato per imburrare il pane, "il martello è cattivo".
Mentre siamo assolutamente d'accordo eval
ha i suoi limiti (ad esempio prestazioni) e rischi (specialmente se il codice di eval inserito dall'utente), ci sono alcune situazioni in cui eval è l'unico modo per ottenere la funzionalità desiderata.
Ad esempio, usano molti motori di template eval
nell'ambito dell'operatore (un altro grande no-no tra gli sviluppatori) per compilare i modelli per le funzioni JavaScript.
Quando stavamo pensando a cosa volevamo dai nostri modelli, abbiamo preso in considerazione diversi approcci. Uno era quello di avere modelli superficiali come Backbone fa con i messaggi emessi sui cambiamenti del modello. Sebbene facili da implementare, questi modelli avrebbero un'utilità limitata - la maggior parte dei modelli di vita reale sono profondi.
Abbiamo preso in considerazione l'utilizzo di semplici oggetti JavaScript con il Object.observe
API (che eliminerebbe la necessità di implementare qualsiasi modello). Mentre la nostra applicazione aveva solo bisogno di lavorare con Chrome, Object.observe
solo di recente è stato abilitato di default - in precedenza era necessario attivare il flag di Chrome, il che avrebbe reso difficile sia la distribuzione che il supporto.
Volevamo modelli che potessimo collegare alle viste ma in modo tale da poter cambiare la struttura della vista senza modificare una singola riga di codice, senza modificare la struttura del modello e senza dover gestire esplicitamente la conversione del modello di vista in modello di dati.
Volevamo anche essere in grado di collegare modelli tra loro (vedere la programmazione reattiva) e di sottoscrivere i cambiamenti del modello. Angular implementa gli orologi confrontando gli stati dei modelli e questo diventa molto inefficiente con modelli grandi e profondi.
Dopo alcune discussioni, abbiamo deciso di implementare la nostra classe del modello che supporterebbe una semplice API get / set per manipolarli e che consentirebbe la sottoscrizione alle modifiche al loro interno:
var m = nuovo modello; m ( 'info.name ') impostare (' angolare').; console.log (m ( 'info') get ().); // logs: name: 'angular' m.on ('. info.name', onNameChange); function onNameChange (msg, data) console.log ('Nome cambiato da', data.oldValue, 'a', data.newValue); m ('. info.name'). set ('milo'); // logs: nome modificato da angular a milo console.log (m.get ()); // logs: info: nome: 'milo' console.log (m ('. info'). get ()); // log: name: 'milo'
Questa API è simile al normale accesso alle proprietà e dovrebbe fornire un accesso sicuro e sicuro alle proprietà: quando ottenere
viene chiamato su percorsi di proprietà inesistenti che restituisce non definito
, e quando impostato
viene chiamato, crea oggetto mancante / albero di array come richiesto.
Questa API è stata creata prima che fosse implementata e lo sconosciuto principale che abbiamo affrontato era come creare oggetti che fossero anche funzioni richiamabili. Si scopre che per creare un costruttore che restituisce oggetti che possono essere chiamati, devi restituire questa funzione dal costruttore e impostarne il prototipo per renderlo un'istanza del costruttore Modello
classe allo stesso tempo:
function Model (data) // modelPath dovrebbe restituire un oggetto ModelPath // con i metodi per ottenere / impostare le proprietà del modello, // per sottoscrivere le modifiche alle proprietà, ecc. var model = function modelPath (path) return new ModelPath (model, sentiero); model .__ proto__ = Model.prototype; model._data = data; model._messenger = new Messenger (model, Messenger.defaultMethods); modello di ritorno; Model.prototype .__ proto__ = Modello .__ proto__;
Mentre il __proto__
la proprietà dell'oggetto di solito è meglio evitarlo, è ancora l'unico modo per cambiare il prototipo dell'istanza dell'oggetto e il prototipo del costruttore.
L'istanza di ModelPath
che deve essere restituito quando viene chiamato il modello (ad es. m ( 'info.name')
sopra) ha presentato un'altra sfida di implementazione. ModelPath
le istanze dovrebbero avere metodi che impostano correttamente le proprietà dei modelli passati al modello quando è stato chiamato (.info.name
in questo caso). Abbiamo preso in considerazione l'implementazione semplicemente analizzando le proprietà passate come stringhe ogni volta che si accede a tali proprietà, ma ci siamo resi conto che avrebbe prodotto prestazioni inefficienti.
Invece, abbiamo deciso di implementarli in modo tale m ( 'info.name')
, per esempio, restituisce un oggetto (un'istanza di ModelPath
"Classe") che ha tutti i metodi di accesso (ottenere
, impostato
, del
e giuntura
) sintetizzato come codice JavaScript e convertito in funzioni JavaScript utilizzando eval
.
Abbiamo anche fatto in modo che tutti questi metodi sintetizzati fossero memorizzati nella cache, quindi una volta utilizzato qualsiasi modello .info.name
tutti i metodi di accesso per questo "percorso di proprietà" sono memorizzati nella cache e possono essere riutilizzati per qualsiasi altro modello.
La prima implementazione del metodo get è stata la seguente:
function sintesizeGetter (path, parsedPath) var getter; var getterCode = 'getter = function value ()' + '\ n var m =' + modelAccessPrefix + '; \ n return'; var modelDataProperty = 'm'; per (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;
Ma il impostato
il metodo sembrava molto peggio ed era molto difficile da seguire, da leggere e da mantenere, perché il codice del metodo creato era fortemente alterato dal codice che generava il metodo. Per questo motivo, abbiamo deciso di utilizzare il motore di templatura doT per generare il codice per i metodi accessor.
Questo è stato il getter dopo il passaggio all'utilizzo di modelli:
var dotDef = modelAccessPrefix: 'this._model._data',; var getterTemplate = 'metodo = funzione value () \ var m = # def.modelAccessPrefix; \ var modelDataProperty = "m"; \ return \ for (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);
Questo si è dimostrato un buon approccio. Ci ha permesso di creare il codice per tutti i metodi di accesso che abbiamo (ottenere
, impostato
, del
e giuntura
) molto modulare e manutenibile.
L'API del modello che abbiamo sviluppato si è dimostrata abbastanza utilizzabile e performante. Si è evoluto per supportare la sintassi degli elementi dell'array, giuntura
metodo per matrici (e metodi derivati, come Spingere
, pop
, ecc.) e interpolazione di accesso proprietà / oggetto.
Quest'ultimo è stato introdotto per evitare di sintetizzare i metodi di accesso (operazione molto più lenta che accede alla proprietà o all'elemento) quando l'unica cosa che cambia è una proprietà o un indice di un elemento. Succederebbe se gli elementi dell'array all'interno del modello dovessero essere aggiornati nel ciclo.
Considera questo esempio:
per (var i = 0; i < 100; i++) var mPath = m('.list[' + i + '].name'); var name = mPath.get(); mPath.set(capitalize(name));
In ogni iterazione, a ModelPath
l'istanza viene creata per accedere e aggiornare la proprietà name dell'elemento array nel modello. Tutte le istanze hanno percorsi di proprietà differenti e richiederà la sintesi di quattro metodi di accesso per ognuno dei 100 elementi che utilizzano eval
. Sarà un'operazione notevolmente lenta.
Con l'interpolazione di accesso proprietà la seconda riga in questo esempio può essere modificata in:
var mPath = m ('. lista [$ 1] .name', i);
Non solo sembra più leggibile, è molto più veloce. Mentre creiamo ancora 100 ModelPath
istanze in questo ciclo, condivideranno tutti gli stessi metodi di accesso, quindi anziché 400 stiamo sintetizzando solo quattro metodi.
Siete invitati a stimare la differenza di prestazioni tra questi campioni.
Milo ha implementato la programmazione reattiva utilizzando modelli osservabili che emettono notifiche su se stessi ogni volta che una delle loro proprietà cambia. Questo ci ha permesso di implementare connessioni dati reattive utilizzando la seguente API:
var connector = minder (m1, '<<<->>> ', m2 ('. info ')); // crea una connessione reattiva bidirezionale // tra il modello m1 e la proprietà ".info" del modello m2 // con la profondità di 2 (proprietà e sotto-proprietà // dei modelli sono collegati).
Come puoi vedere dalla riga precedente, ModelPath
restituito da m2 ( 'info')
dovrebbe avere la stessa API del modello, il che significa che ha la stessa API di messaggistica del modello ed è anche una funzione:
var mPath = m ('. info); mPath ('. name'). set ("); // imposta poperty '.info.name' in m mPath.on ('. name', onNameChange); // uguale a m ('. info.name') .on (", onNameChange) // come m.on ('. info.name', onNameChange);
In modo simile, possiamo collegare i modelli alle viste. I componenti (vedi la prima parte della serie) possono avere un aspetto dei dati che funge da API per manipolare DOM come se fosse un modello. Ha la stessa API del modello e può essere utilizzato nelle connessioni reattive.
Quindi questo codice, ad esempio, collega una vista DOM a un modello:
var connector = minder (m, '<<<->>> ', comp.data);
Sarà illustrato più dettagliatamente di seguito nell'applicazione To-Do di esempio.
Come funziona questo connettore? Sotto il cofano, il connettore semplicemente sottoscrive le modifiche nelle origini dati su entrambi i lati della connessione e passa le modifiche ricevute da un'origine dati a un'altra origine dati. Un'origine dati può essere un modello, un percorso del modello, un aspetto dei dati del componente o qualsiasi altro oggetto che implementa la stessa API di messaggistica del modello.
La prima implementazione del connettore era abbastanza semplice:
// ds1 e ds2 - connected datasources // mode definisce la direzione e la profondità della funzione di connessione Connector (ds1, mode, ds2) var parsedMode = mode.match (/ ^ (\<*)\-+(\>*) $ /); _.extend (this, ds1: ds1, ds2: ds2, mode: mode, depth1: parsedMode [1] .length, depth2: parsedMode [2] .length, isOn: false); this.on (); _.extendProto (Connector, on: on, off: off); function on () var subscriptionPath = this._subscriptionPath = new Array (this.depth1 || this.depth2) .join ('*'); var auto = questo; if (this.depth1) linkDataSource ('_ link1', '_link2', this.ds1, this.ds2, subscriptionPath); if (this.depth2) linkDataSource ('_ link2', '_link1', this.ds2, this.ds1, subscriptionPath); this.isOn = true; function linkDataSource (linkName, stopLink, linkToDS, linkedDS, subscriptionPath) var onData = funzione onData (percorso, dati) // impedisce il ciclo infinito del messaggio // per le connessioni bidirezionali se (onData .__ stopLink) restituisce; var dsPath = linkToDS.path (percorso); if (dsPath) self [stopLink] .__ stopLink = true; dsPath.set (data.newValue); delete self [stopLink] .__ stopLink; linkedDS.on (subscriptionPath, onData); self [linkName] = onData; return onData; function off () var self = this; unlinkDataSource (this.ds1, '_link2'); unlinkDataSource (this.ds2, '_link1'); this.isOn = falso; function unlinkDataSource (linkedDS, linkName) if (self [linkName]) linkedDS.off (self._subscriptionPath, self [linkName]); elimina self [nome collegamento];
Ormai, le connessioni reattive in milo si sono sostanzialmente evolute: possono cambiare le strutture dei dati, cambiare i dati stessi e anche eseguire convalide dei dati. Questo ci ha permesso di creare un potente generatore di UI / form che abbiamo intenzione di creare anche open source.
Molti di voi saranno a conoscenza del progetto TodoMVC: una raccolta di implementazioni di app To-Do realizzate utilizzando una varietà di diversi framework MV *. L'app To-Do è un test perfetto di qualsiasi framework dato che è abbastanza semplice da costruire e confrontare, ma richiede una gamma abbastanza ampia di funzionalità tra cui operazioni CRUD (creazione, lettura, aggiornamento ed eliminazione), interazione DOM e visualizzazione / modello vincolante solo per citarne alcuni.
In varie fasi dello sviluppo di Milo, abbiamo cercato di creare semplici applicazioni To-Do e, senza esitazioni, ha evidenziato bug o difetti del framework. Anche nel nostro progetto principale, quando Milo veniva usato per supportare un'applicazione molto più complessa, abbiamo trovato piccoli bug in questo modo. Ormai, il framework copre la maggior parte delle aree richieste per lo sviluppo di applicazioni web e troviamo il codice necessario per costruire l'app To-Do per essere abbastanza succinta e dichiarativa.
Prima di tutto, abbiamo il codice HTML. È uno standard HTML standard con un po 'di stile per gestire gli elementi selezionati. Nel corpo abbiamo un ml-bind
attributo per dichiarare la lista delle cose da fare, e questo è solo un semplice componente con il elenco
aspetto aggiunto. Se volessimo avere più liste, dovremmo probabilmente definire una classe componente per questa lista.
All'interno della lista c'è il nostro articolo campione, che è stato dichiarato utilizzando una custom Fare
classe. Mentre la dichiarazione di una classe non è necessaria, rende la gestione dei figli del componente molto più semplice e modulare.
To-Do di
Per farci correre milo.binder ()
ora, prima dovremo definire il Fare
classe. Questa classe avrà bisogno di avere il articolo
aspetto, e sarà fondamentalmente responsabile della gestione del pulsante Elimina e della casella di controllo che si trova su ciascuno Fare
.
Prima che un componente possa operare sui suoi figli, è necessario prima attendere il childrenbound
evento da licenziare. Per ulteriori informazioni sul ciclo di vita dei componenti, consultare la documentazione (collegamento ai documenti del componente).
// Creazione di una nuova classe di componenti sfaccettati con il facet "item". // Questo di solito è definito nel proprio file. // Nota: il facet item richiederà 'in // il' container ',' data 'e' dom 'facet var Todo = _.createSubclass (milo.Component,' Todo '); milo.registry.components.add (Todo); // Aggiunta del nostro metodo init personalizzato _.extendProto (Todo, init: Todo $ init); function Todo $ init () // Chiamando il metodo init ereditato. milo.Component.prototype.init.apply (this, arguments); // L'ascolto di 'childrenbound' che viene attivato dopo il raccoglitore // ha finito con tutti i figli di questo componente. this.on ('childrenbound', function () // Otteniamo l'ambito (i componenti figlio vivono qui) var scope = this.container.scope; // E configura due abbonamenti, uno per i dati della checkbox // La sintassi della sottoscrizione consente al contesto di passare scope.checked.data.on (", subscriber: checkTodo, context: this); // e uno all'evento" click "del pulsante di eliminazione. Scope.deleteBtn.events.on ('click', subscriber: removeTodo, context: this);); // Quando la casella di controllo cambia, imposteremo la classe della funzione Todo di conseguenza checkTodo (path, data) this.el.classList.toggle ('todo-item-checked', data.newValue); // Per rimuovere l'elemento, usiamo il metodo 'removeItem' della funzione facet 'item' removeTodo (eventType, event) this.item.removeItem () ;
Ora che abbiamo questa configurazione, possiamo chiamare il raccoglitore per collegare componenti agli elementi DOM, creare un nuovo modello con una connessione bidirezionale alla lista tramite la sua sfaccettatura dati.
// La funzione Milo ready funziona come la funzione ready di jQuery. milo (function () // Call binder sul documento. // Allega componenti agli elementi DOM con l'attributo ml-bind var scope = milo.binder (); // Ottieni l'accesso ai nostri componenti tramite l'oggetto scope var todos = scope.todos // Elenco Todos, newTodo = scope.newTodo // Nuovo input todo, addBtn = scope.addBtn // Pulsante Aggiungi, modelView = scope.modelView; // Dove stampiamo il modello // Configura il nostro modello, questo sarà tenere la matrice di todos var m = new milo.Model; // Questa sottoscrizione ci mostrerà il contenuto del // modello in ogni momento sotto il todos m.on (/.*/, function showModel (msg, data) modelView.data.set (JSON.stringify (m.get ()));); // Crea una connessione bidirezionale profonda tra il nostro modello e l'elenco dati di tutti i tipi. // I galloni più interni mostrano la direzione della connessione (può anche in un modo), // il resto definisce la profondità di connessione - 2 livelli in questo caso, per includere // le proprietà degli elementi dell'array. milo.minder (m, '<<<->>> ', todos.data); // Sottoscrizione per fare clic sull'evento del pulsante aggiungi addBtn.events.on ('click', addTodo); // Fare clic sul gestore della funzione del pulsante aggiungi addTodo () // Imballiamo l'input 'newTodo' come oggetto // La proprietà 'testo' corrisponde al markup dell'elemento. var itemData = text: newTodo.data.get (); // Inseriamo questi dati nel modello. // La vista verrà aggiornata automaticamente! m.push (itemData); // E infine imposta nuovamente l'input in bianco. newTodo.data.set ("););
Questo esempio è disponibile in jsfiddle.
Il campione To-Do è molto semplice e mostra una parte molto piccola della straordinaria potenza di Milo. Milo ha molte funzionalità non trattate in questo e negli articoli precedenti, tra cui trascinamento della selezione, archiviazione locale, utility http e websockets, utilità DOM avanzate, ecc..
Oggi milo alimenta il nuovo CMS di dailymail.co.uk (questo CMS ha decine di migliaia di codice javascript front-end e viene utilizzato per creare più di 500 articoli ogni giorno).
Milo è open source e ancora in una fase beta, quindi è un buon momento per sperimentarlo e magari anche contribuire. Ci piacerebbe il tuo feedback.
Nota che questo articolo è stato scritto da Jason Green e Evgeny Poberezkin.