Reaktiv programmering

I den första delen av serien pratade vi om komponenter som låter dig hantera olika beteenden med hjälp av fasetter och hur Milo hanterar meddelanden.

I den här artikeln ser vi på ett annat vanligt problem när det gäller att utveckla webbläsarprogram: anslutning av modeller till visningar. Vi kommer att unravela några av de "magiska" som gör det möjligt att göra dubbelriktad databindning i Milo, och för att paketera upp saker, bygger vi en fullt fungerande applikation på mindre än 50 kodlinjer.

Modeller (eller Eval är inte ond)

Det finns flera myter om JavaScript. Många utvecklare tror att eval är ont och aldrig ska användas. Den troen leder till att många utvecklare inte kan säga när eval kan och ska användas.

Mantrar som "eval är ondskan "kan bara vara skadligt när vi har att göra med något som i huvudsak är ett verktyg. Ett verktyg är bara "bra" eller "dåligt" när det ges ett sammanhang. Du skulle inte säga att en hammare är ont, eller hur? Det beror verkligen på hur du använder den. När det används med nagel och några möbler är "hammare bra". När du brukar smör ditt bröd, är "hammeren dålig".

Medan vi definitivt håller med om det eval har sina begränsningar (t ex prestanda) och risker (speciellt om vi skriver in eval-kod av användaren) finns det en hel del situationer när eval är det enda sättet att uppnå önskad funktionalitet.

Till exempel använder många templerande motorer eval inom ramen för med operatören (en annan stor nej bland utvecklare) att sammanställa mallar till JavaScript-funktioner.

När vi tänkte på vad vi ville ha från våra modeller betraktade vi flera tillvägagångssätt. En var att ha grunda modeller som Backbone gör med meddelanden som emitteras vid modelländringar. Medan de är enkla att implementera, skulle dessa modeller ha begränsad användbarhet - de flesta verkliga modeller är djupa.

Vi övervägde att använda vanliga JavaScript-objekt med Object.observe API (vilket skulle eliminera behovet av att implementera några modeller). Även om vår ansökan bara behövde fungera med Chrome, Object.observe först nyligen aktiverades som standard - tidigare krävde du att du slog på Chrome-flaggan, vilket skulle ha gjort både implementering och support svårt.

Vi ville ha modeller som vi kunde ansluta till synpunkter, men på ett sådant sätt att vi kunde ändra visningsstrukturen utan att ändra en enda kodrad utan att ändra modellens struktur och utan att uttryckligen hantera omvandlingen av visningsmodellen till datamodell.

Vi ville också kunna ansluta modeller till varandra (se reaktiv programmering) och att prenumerera på modelländringar. Vinklar implementerar klockor genom att jämföra modellerna och det blir mycket ineffektivt med stora, djupa modeller.

Efter en diskussion bestämde vi oss för att vi skulle implementera vår modellklass som skulle stödja ett enkelt få / set API för att manipulera dem och det skulle möjliggöra att prenumerera på förändringar inom dem:

var m = ny modell; m ( 'info.name ') in (' vinkel.'); console.log (m ( 'info') get ().); // loggar: namn: 'vinkel' m.on ('. info.name', onNameChange); funktion påNameChange (msg, data) console.log ('Namn ändrat från', data.oldValue, 'to', data.newValue);  m ('. info.name'). set ('milo'); // loggar: Namn ändras från vinkel till milo console.log (m.get ()); // loggar: info: name: 'milo' console.log (m ('info'). get ()); // loggar: namn: 'milo'

Detta API liknar normal tillgång till tillgången och bör ge säker tillgång till fastigheter - när skaffa sig kallas på obefintliga fastighetsvägar som den återvänder odefinierad, och när uppsättning kallas, det skapar saknade objekt / matris efter behov.

Detta API skapades innan det implementerades och det viktigaste okända som vi mötte var hur man skapade objekt som även var uppkallningsbara funktioner. Det visar sig att för att skapa en konstruktör som returnerar objekt som kan kallas, måste du returnera denna funktion från konstruktören och ställa in sin prototyp för att göra det till en förekomst av Modell klass på samma gång:

Funktionsmodell (data) // modelPath bör returnera ett ModelPath-objekt // med metoder för att få / ställa in modellegenskaper, // för att prenumerera på egendomsförändringar etc. varmodell = funktionsmodellPath (sökväg) returnera ny ModelPath (modell, väg);  modell .__ proto__ = Model.prototype; model._data = data; model._messenger = new Messenger (modell, Messenger.defaultMethods); returmodell;  Model.prototype .__ proto__ = Modell .__ proto__;

Medan __proto__ objektets egenskap är vanligtvis bättre att undvika, det är fortfarande det enda sättet att ändra prototypen av objektet förekomst och konstruktörens prototyp.

Förekomsten av ModelPath som bör returneras när modellen heter (t.ex.. m ( 'info.name') ovan) presenterade en annan implementeringsutmaning. ModelPath instanser borde ha metoder som korrekt fastställde egenskaper hos modeller som passerade till modell när det kallades (.info.name I detta fall). Vi övervägde att implementera dem genom att helt enkelt analysera egenskaper som passerade som strängar när dessa egenskaper nås, men vi insåg att det skulle ha resulterat i ineffektiva prestanda.

I stället bestämde vi oss för att genomföra dem på ett sådant sätt att m ( 'info.name'), till exempel returnerar ett objekt (en instans av ModelPath "Klass") som har alla accessor metoder (skaffa sig, uppsättning, del och splitsa) syntetiseras som JavaScript-kod och konverteras till JavaScript-funktioner med eval.

Vi gjorde också alla dessa syntetiserade metoder cachade så en gång någon modell användes .info.name alla accessor metoder för denna "egenskapsväg" är cachade och kan återanvändas för någon annan modell.

Det första genomförandet av få-metoden såg ut så här:

funktion synthesizeGetter (sökväg, parsedPath) var getter; var getterCode = 'getter = funktionsvärde ()' + '\ n var m =' + modelAccessPrefix + '; \ n returnera'; var modelDataProperty = 'm'; för (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; 

Men uppsättning Metoden såg väldigt sämre ut och var väldigt svår att följa, att läsa och underhålla, eftersom koden för den skapade metoden var kraftigt avgränsad med koden som genererade metoden. På grund av detta bytte vi till att använda dT-templerande motor för att generera koden för accessor-metoder.

Detta var getteren efter att ha bytt till att använda mallar:

var dotDef = modelAccessPrefix: 'this._model._data',; var getterTemplate = 'metod = funktionsvärde () \ var m = # def.modelAccessPrefix; \ var modelDataProperty = "m";  \ return \ för (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); 

Detta visade sig vara ett bra tillvägagångssätt. Det fick oss att göra koden för alla accessor metoder vi har (skaffa sig, uppsättning, del och splitsa) Mycket modulärt och underhållbart.

Modul API vi utvecklade visade sig vara ganska användbar och prestanda. Det utvecklades för att stödja arrayelementens syntax, splitsa metod för arrays (och härledda metoder, såsom tryck, pop-, etc.), och interpolation av egenskap / objektåtkomst.

Den senare introducerades för att undvika att syntetisera accessormetoder (vilket är mycket långsammare operation som åtkomst till egendom eller objekt) när det enda som ändras är en del egendom eller artikelindex. Det skulle hända om matriselement i modellen måste uppdateras i slingan.

Tänk på detta exempel:

för (var i = 0; i < 100; i++)  var mPath = m('.list[' + i + '].name'); var name = mPath.get(); mPath.set(capitalize(name)); 

I varje iteration, a ModelPath Instans skapas för att få åtkomst till och uppdatera namnegenskap hos matriselementet i modellen. Alla instanser har olika fastighetsvägar och det kommer att behöva syntetisera fyra accessor metoder för var och en av de 100 elementen som används eval. Det kommer att bli en väldigt långsam operation.

Med tillgångsinterpolering kan andra raden i detta exempel ändras till:

var mPath = m ('. lista [$ 1] .name', i);

Det verkar inte bara mer läsligt, det är mycket snabbare. Medan vi fortfarande skapar 100 ModelPath instanser i denna loop, alla kommer att dela samma accessor metoder, så istället för 400 syntetiserar vi bara fyra metoder.

Du är välkommen att uppskatta prestationsskillnaden mellan dessa prover.

Reaktiv programmering

Milo har genomfört reaktiv programmering med observerbara modeller som avger meddelanden om sig när någon av deras egenskaper förändras. Det här har gjort det möjligt för oss att implementera reaktiva dataanslutningar med hjälp av följande API:

var-kontakt = minder (m1, '<<<->>> ', m2 ('. info ')); // skapar dubbelriktad reaktiv anslutning // mellan modell m1 och egendom ".info" av modell m2 // med djupet 2 (egenskaper och delegenskaper // av modellerna är anslutna).

Som du kan se från ovanstående linje, ModelPath returneras av m2 ( ". info) borde ha samma API som modellen, vilket innebär att samma meddelandeprogram är som modell och är också en funktion:

var mPath = m ('. info); mPath ('.name'). set ("); // set poperty '.info.name' i m mPath.on ('.name', onNameChange); // samma som m ('.info.name') .on (", onNameChange) // samma som m.on ('. info.name', onNameChange);

På liknande sätt kan vi ansluta modeller till synpunkter. Komponenterna (se den första delen av serien) kan ha en datafaset som fungerar som ett API för att manipulera DOM som om det var en modell. Den har samma API som modell och kan användas i reaktiva anslutningar.

Så den här koden kopplar till exempel en DOM-vy till en modell:

var anslutning = minder (m, '<<<->>> ', comp.data);

Det kommer att demonstreras mer detaljerat nedan i provet To-Do-ansökan.

Hur fungerar den här kontakten? Under huven ansluts kopplingen helt enkelt till förändringarna i datakällorna på båda sidor av anslutningen och överför ändringarna från en datakälla till en annan datakälla. En datakälla kan vara en modell, modellväg, datafasett för komponenten eller något annat objekt som implementerar samma meddelandehanterings API som modell gör.

Det första genomförandet av kontakten var ganska enkelt:

// ds1 och ds2 - anslutna datakällor // -läget definierar riktningen och anslutningsfunktionsdjupet Connector (ds1, mode, ds2) var parsedMode = mode.match (/ ^ (\<*)\-+(\>*) $ /); _.extend (detta, ds1: ds1, ds2: ds2, läge: läge, djup1: parsedMode [1] .längd, deep2: parsedMode [2] .length, isOn: false); this.on ();  _.extendProto (Connector, on: on, off: off); funktion på () var subscriptionPath = this._subscriptionPath = new Array (this.depth1 || this.depth2) .join ('*'); var själv = detta; om (this.depth1) linkDataSource ('_ link1', '_link2', this.ds1, this.ds2, subscriptionPath); om (this.depth2) linkDataSource ('_ link2', '_link1', this.ds2, this.ds1, subscriptionPath); this.isOn = true; funktion linkDataSource (länknamn, stopLink, linkToDS, linkedDS, subscriptionPath) var onData = funktion onData (sökväg, data) // förhindrar ändlösa meddelelsessling // för dubbelriktad anslutning om (onData .__ stopLink) returnerar; var dsPath = linkToDS.path (bana); om (dsPath) self [stopLink] .__ stopLink = true; dsPath.set (data.newValue); radera själv [stopLink] .__ stopLink; linkedDS.on (subscriptionPath, onData); själv [linkName] = onData; returnera onData;  funktion av () var själv = detta; unlinkDataSource (this.ds1, '_link2'); unlinkDataSource (this.ds2, '_link1'); this.isOn = false; funktion unlinkDataSource (linkedDS, länknamn) if (self [linkName]) linkedDS.off (self._subscriptionPath, self [linkName]); ta bort själv [länknamn]; 

Hittills har de reaktiva anslutningarna i milo utvecklats väsentligt - de kan ändra datastrukturer, ändra data själva och även utföra datavalideringar. Detta har gjort det möjligt för oss att skapa en mycket kraftfull UI / formgenerator som vi planerar att göra open source också.

Bygg en till-gör-app

Många av er kommer att vara medvetna om TodoMVC-projektet: En samling To-Do-implementeringar gjorda med en mängd olika MV-ramar. Verktygsprogrammet är ett perfekt test av alla ramar, eftersom det är ganska enkelt att bygga och jämföra, men kräver ett ganska brett utbud av funktionalitet, inklusive CRUD (skapa, läsa, uppdatera och ta bort), DOM-interaktion och visning / modell bindande för att bara nämna några.

Vid olika stadier av utvecklingen av Milo försökte vi bygga enkla To-Do-applikationer, och utan tvekan framhöll det rambuller eller brister. Till och med djupt in i vårt huvudprojekt, när Milo användes för att stödja en mycket mer komplex applikation, har vi hittat små buggar på så sätt. Ramverket täcker nu de flesta områden som krävs för webbapplikationsutveckling och vi finner den kod som krävs för att bygga Applikationen för att vara ganska kortfattad och deklarativ.

Först av, vi har HTML-märkning. Det är en vanlig HTML-panna med lite styling för att hantera kontrollerade objekt. I kroppen har vi en ml-bind attribut att deklarera listan Att göra, och det här är bara en enkel komponent med lista fasett tillagd. Om vi ​​ville ha flera listor, borde vi förmodligen definiera en komponent klass för den här listan.

Inne i listan är vårt provobjekt, som har deklarerats med hjälp av en anpassad Att göra klass. Medan det inte är nödvändigt att förklara en klass, gör det att hanteringen av komponentens barn är mycket enklare och modulär.

            

Att Göra

Modell

För att vi ska springa milo.binder () nu måste vi först definiera Att göra klass. Denna klass kommer att behöva ha Artikel faset och kommer i grund och botten att vara ansvarig för hanteringen av raderingsknappen och kryssrutan som finns på var och en Att göra.

Innan en komponent kan fungera på sina barn måste den först vänta på childrenbound händelse att sparkas på den. Mer information om komponentens livscykel finns i dokumentationen (länk till komponentdokument).

// Skapa en ny fasettkomponentklass med objektens fasett. // Det här brukar definieras i sin egen fil. // Obs! Objektets fasett kommer att kräva i // "behållarens", "data" och "dom" -facetterna var Todo = _.createSubclass (milo.Component, 'Todo'); milo.registry.components.add (Todo); // Lägga till vår egen anpassade init-metod _.extendProto (Todo, init: Todo $ init); funktion Todo $ init () // Kallar den ärvda init-metoden. milo.Component.prototype.init.apply (detta, argument); // Lyssna för 'barnbundet' som avfyras efter bindemedel // har slutförts med alla barn i denna komponent. this.on ('childrenbound', function () // Vi får omfattningen (barnkomponenterna bor här) var scope = this.container.scope; // Och konfigurera två prenumerationer, en till data i kryssrutan // Abonnementssyntaxen tillåter att kontextet passeras scope.checked.data.on (", abonnent: checkTodo, context: this); // och en till delete-knappen" click "-händelse. Scope.deleteBtn.events.on ("klick", abonnent: removeTodo, context: this);; // När kryssrutan ändras sätter vi klassen i Todo i enlighet med funktionskontrollTodo (sökväg, data) this.el.classList.toggle ('todo-item-checked', data.newValue); // För att ta bort objektet använder vi "removeItem" -metoden för "objekt" -fasfunktionen removeTodo (eventType, event) this.item.removeItem ;

Nu när vi har den inställningen kan vi ringa bindemedlet för att bifoga komponenter till DOM-element, skapa en ny modell med tvåvägsanslutning till listan via dess datasats.

// Milo redo funktion, fungerar som jQuerys färdiga funktion. milo (funktion () // Ring bindemedel på dokumentet. // Det bifogar komponenter till DOM-element med ml-bind attribut var scope = milo.binder (); // Få tillgång till våra komponenter via objektivet var vardera = scope.todos // Todos list, newTodo = scope.newTodo // Ny todo-inmatning, addBtn = scope.addBtn // Add button, modelView = scope.modelView; // Var vi skriver ut modell // Ange vår modell, det här kommer håll arrayen av todos var m = ny milo.Model; // Denna prenumeration visar oss innehållet i // modellen hela tiden under todos m.on (/.*/, funktion showModel (msg, data)  modelView.data.set (JSON.stringify (m.get ());); // Skapa en djup tvåvägs bindning mellan vår modell och todos listdata faset. // De innersta chevronsna visar anslutningsriktning också vara ett sätt), // resten definierar anslutningsdjupet - 2 nivåer i det här fallet, för att inkludera // egenskaperna för arrayobjekt. milo.minder (m, '<<<->>> ', todos.data); // Prenumeration för att klicka på händelsen för add-knappen addBtn.events.on ('click', addTodo); // Klicka hanterare av add-button funktion addTodo () // Vi paketerar 'newTodo' input som ett objekt // Egenskapen 'text' motsvarar objektmarkeringen. var itemData = text: newTodo.data.get (); // Vi trycker in data i modellen. // Utsikten uppdateras automatiskt! m.push (ItemData); // Och slutligen ställa in inmatningen till tom igen. newTodo.data.set (");); 

Detta prov är tillgängligt i jsfiddle.

Slutsats

Att göra prov är väldigt enkelt och det visar en mycket liten del av Milos enorma makt. Milo har många funktioner som inte omfattas av detta och de tidigare artiklarna, inklusive dra och släpp, lokal lagring, http och websocketsverktyg, avancerade DOM-verktyg etc.

Nuförtiden milo driver det nya CMS av dailymail.co.uk (detta CMS har tiotusentals främre javascript-kod och används för att skapa mer än 500 artiklar varje dag).

Milo är öppen källkod och fortfarande i en beta-fas, så det är en bra tid att experimentera med det och kanske till och med bidra. Vi skulle älska din feedback.


Observera att denna artikel skrevs av både Jason Green och Evgeny Poberezkin.