Att bygga ett ramverk från grunden är inte något vi specifikt bestämde oss för att göra. Du måste vara galen, eller hur? Med den uppsjö av JavaScript-ramar där ute, vilken möjlig motivation kan vi ha för att rulla oss själva?
Vi letade ursprungligen efter en ram för att bygga det nya innehållshanteringssystemet för Daily Mail-webbplatsen. Huvudsyftet var att göra redigeringsprocessen mycket mer interaktiv med alla element i en artikel (bilder, inbyggnader, utropslådor och så vidare) som är dragbara, modulära och självstyrande.
Alla de ramar som vi kunde ta hand om var konstruerade för mer eller mindre statiska användargränssnitt definierade av utvecklare. Vi behövde skapa en artikel med både redigerbar text och dynamiskt gjorda användargränssnitt.
Ryggraden var för låg nivå. Det gjorde lite mer än att tillhandahålla grundläggande objekt struktur och budskap. Vi skulle behöva bygga mycket abstraktion ovanför Backbone-fundamentet, så vi bestämde oss för att vi hellre skulle bygga denna fundament själv.
AngularJS blev vårt ramverk av val för att bygga små till medelstora webbläsarprogram som har relativt statiska användargränssnitt. Tyvärr är AngularJS väldigt mycket en svart låda - det avslöjar inte något lämpligt API för att utöka och manipulera de objekt som du skapar med det - direktiv, kontroller, tjänster. Även om AngularJS tillhandahåller reaktiva kopplingar mellan visningar och räckviddsuttryck, tillåter det inte att definiera reaktiva kopplingar mellan modeller, så någon tillämpning av mediestorlek blir mycket lik en jQuery-applikation med spagetti av händelsehörare och återuppringningar, med den enda skillnaden att I stället för händelselyttare har en vinkelapplikation tittare och istället för att manipulera DOM manipulerar du scopes.
Det vi alltid ville ha var en ram som skulle tillåta;
Vi kunde inte hitta vad vi behövde i befintliga lösningar, så vi började utveckla Milo parallellt med den applikation som använder den.
Milo valdes som namn på grund av Milo Minderbinder, en krigsperspektiv från Fångst 22 av Joseph Heller. Efter att ha börjat från att hantera röraoperationer utvidgade han dem till ett lönsamt handelsföretag som kopplade alla med allt, och att Milo och alla andra "har en andel".
Milo ramverket har modulbindaren, som binder DOM-element till komponenter (via special ml-bind
attribut), och modulen minder som möjliggör etablering av levande reaktiva kopplingar mellan olika datakällor (modell och data fas av komponenter är sådana datakällor).
Tillfälligt kan Milo läsas som en akronym av MaIL Online, och utan den unika arbetsmiljön på Mail Online kunde vi aldrig ha byggt upp det.
Visningar i Milo hanteras av komponenter, som i grunden förekommer i JavaScript-klasser, som ansvarar för att hantera ett DOM-element. Många ramverk använder komponenter som ett koncept för att hantera UI-element, men det mest uppenbara som kommer att tänka på är Ext JS. Vi hade arbetat utförligt med Ext JS (den gamla applikationen vi ersatte byggdes med) och ville undvika det som vi ansåg vara två nackdelar med dess tillvägagångssätt.
Det första är att Ext JS inte gör det enkelt för dig att hantera din uppmärksamhet. Det enda sättet att bygga ett användargränssnitt är att sammanställa inbyggda hierarkier av komponentkonfigurationer. Detta leder till onödigt komplex gjord markering och tar kontroll över utvecklarens händer. Vi behövde en metod för att skapa komponenter inline, i vår egen, handgjorda HTML-markup. Det här är där bindemedel kommer in.
Binder skannar vår markup letar efter ml-bind
attribut så att det kan instansera komponenter och binda dem till elementet. Attributet innehåller information om komponenterna; Detta kan innehålla komponentklassen, fasetterna och måste innehålla komponentnamnet.
Vår milokomponent
Vi pratar om facetter om en minut, men för nu kan vi se hur vi kan ta detta attributvärde och extrahera konfigurationen från det med ett vanligt uttryck.
var bindAttrRegex = / ^ ([^ \: \ [\]] *) (?: \ [([^ \: \ [\]] *) \])? \:? ([^:] *) $ / ; var result = value.match (bindAttrRegex); // Resultatet är en array med // resultat [0] = 'ComponentClass [facet1, facet2]: componentName'; // resultat [1] = 'ComponentClass'; // resultat [2] = 'facet1, facet2'; // resultat [3] = 'komponentnamn';
Med den informationen är allt vi behöver göra för att iterera över alla ml-bind
attribut, extrahera dessa värden och skapa instanser för att hantera varje element.
var bindAttrRegex = / ^ ([^ \: \ [\]] *) (?: \ [([^ \: \ [\]] *) \])? \:? ([^:] *) $ / ; funktionsbinder (återuppringning) var scope = ; // vi får alla element med attributet ml-bind var els = document.querySelectorAll ('[ml-bind]'); Array.prototype.forEach.call (els, funktion (el) var attrText = el.getAttribute ('ml-bind'); var result = attrText.match (bindAttrRegex); var className = resultat [1] || 'Komponent '; var facets = resultat [2] .split (', '); var compName = resultat [3]; // förutsatt att vi har ett registerobjekt för alla våra klasser var comp = ny klassRegistry [className] (el); comp .addFacets (facets); comp.name = compName; scope [compName] = comp; // vi hänvisar till komponenten på elementet el .___ milo_component = comp;); callback (räckvidd); bindemedel (funktion (räckvidd) console.log (scope););
Så med bara lite regex och lite DOM-traversal kan du skapa din egen mini-ram med anpassad syntax för att passa din specifika affärslogik och kontext. I mycket liten kod har vi en arkitektur som möjliggör modulära, självstyrande komponenter, som kan användas men du vill. Vi kan skapa praktisk och deklarativ syntax för att instansera och konfigurera komponenter i vår HTML, men till skillnad från vinkel kan vi hantera dessa komponenter men vi gillar.
Den andra sak vi tyckte om Ext JS var att den har en mycket brant och styv klasshierarki, vilket skulle ha gjort det svårt att organisera våra komponentklasser. Vi försökte skriva en lista över alla beteenden som någon komponent i en artikel kan ha. En komponent kan till exempel redigeras, det kan lyssna på händelser, det kan vara ett droppmål eller vara draget i sig. Dessa är bara några av de beteenden som behövs. En preliminär lista som vi skrev upp hade cirka 15 olika typer av funktionalitet som kan krävas av en viss komponent.
Att försöka organisera dessa beteenden i någon form av hierarkisk struktur skulle ha varit inte bara en stor huvudvärk, men också mycket begränsande bör vi någonsin vilja ändra funktionaliteten för en viss komponentklass (något vi slutade göra mycket). Vi bestämde oss för att genomföra ett mer flexibelt objektorienterat designmönster.
Vi hade läst upp ansvarsdriven design, som i motsats till den vanligare modellen för att definiera beteendet hos en klass tillsammans med de uppgifter som den innehar är mer oroad över de åtgärder som ett objekt ansvarar för. Det passade oss bra när vi hade att göra med en komplex och oförutsägbar datamodell, och detta tillvägagångssätt skulle göra det möjligt för oss att lämna implementeringen av dessa detaljer till senare.
Det viktigaste vi tog bort från RDD var konceptet Roller. En roll är en uppsättning relaterade ansvarsområden. När det gäller vårt projekt identifierade vi roller som redigering, dra, släppzon, valbar eller händelser bland många andra. Men hur representerar du dessa roller i kod? För det lånade vi från dekoratormönstret.
Dekoreringsmönstret gör att beteende kan läggas till ett enskilt objekt, antingen statiskt eller dynamiskt, utan att påverka beteendet hos andra objekt från samma klass. Nu medan körningshanteringen av klassbeteende inte har varit särskilt nödvändig i detta projekt var vi väldigt intresserade av den typ av inkapsling som denna idé ger. Milos implementering är en typ av hybrid som involverar objekt som kallas fasetter, som är bifogade som egenskaper till komponentinstansen. Fasaden får en referens till komponenten, det är "ägare" och ett konfigurationsobjekt som gör det möjligt för oss att anpassa fasetter för varje komponentklass.
Du kan tänka på fasetter som avancerade, konfigurerbara mixins som får sin egen namnrymd på sina ägarobjekt och till och med sina egna i det
metod, som måste skrivas över av fasettunderklassen.
funktionen Facet (ägare, config) this.name = this.constructor.name.toLowerCase (); this.owner = ägare; this.config = config || ; this.init.apply (detta, argument); Facet.prototype.init = funktionen Facet $ init () ;
Så vi kan subclass denna enkla Aspekt
klass och skapa specifika aspekter för varje typ av beteende som vi vill ha. Milo kommer prebuilt med en mängd olika fasetter, till exempel DOM
facet, som tillhandahåller en samling av DOM-verktyg som fungerar på ägarkomponentens element, och Lista
och Artikel
fasetter som arbetar tillsammans för att skapa listor över upprepande komponenter.
Dessa facetter sammanföres sedan av vad vi kallade en FacetedObject
, vilken är en abstrakt klass från vilken alla komponenter ärva. De FacetedObject
har en klassmetod kallad createFacetedClass
som helt enkelt subklassar sig själv och fäster alla fasetter till a fasetter
egendom på klassen. På så sätt när FacetedObject
får instantiated, det har tillgång till alla dess fasett klasser, och kan iterera dem att bootstrap komponenten.
funktion FacetedObject (facetsOptions / *, andra init args * /) facetsOptions = facetsOptions? _.clone (facetsOptions): ; var thisClass = this.constructor, facets = ; om (! thisClass.prototype.facets) kasta nytt fel ("Inga fasetter definierade"); _.eachKey (this.facets, instantiateFacet, detta, sant); Object.defineProperties (detta, fasetter); om (this.init) this.init.apply (detta, argument); funktion instantiateFacet (facetClass, fct) var facetOpts = facetsOptions [fct]; ta bort facetsOptions [fct]; fasetter [fct] = uppräknat: falskt, värde: ny facetClass (detta, facetOpts); FacetedObject.createFacetedClass = funktion (namn, facetsClasses) var FacetedClass = _.createSubclass (detta, namn, sant); _.extendProto (FacetedClass, facets: facetsClasses); returnera FacetedClass; ;
I Milo abstrakte vi lite längre genom att skapa en bas Komponent
klass med en matchning createComponentClass
klassmetoden, men grundprincipen är densamma. Med viktiga beteenden som hanteras av konfigurerbara fasetter kan vi skapa många olika komponentklasser i en deklarativ stil utan att behöva skriva för mycket anpassad kod. Här är ett exempel med hjälp av några av de out-of-the-box-fasetter som följer med Milo.
var Panel = Component.createComponentClass ('Panel', dom: cls: 'my-panel', tagName: 'div', händelser: messages: 'click': onPanelClick, dra: messages: ..., släpp: messages: ..., container: undefined);
Här har vi skapat en komponent klass kallad Panel
, som har tillgång till DOM-verktygsmetoder, ställer automatiskt in sin CSS-klass i det
, det kan lyssna på DOM-evenemang och installera en klickhanterare på i det
, Det kan släpas runt och fungera som ett droppmål. Den sista fasaden där, behållare
säkerställer att denna komponent sätter upp sin egen räckvidd och kan i själva verket ha barnkomponenter.
Vi hade diskuterat ett tag om huruvida alla komponenter som bifogas dokumentet ska bilda en platt struktur eller ska bilda ett eget träd, där barn endast är tillgängliga från deras förälder.
Vi skulle definitivt ha behov för vissa situationer, men det kunde ha hanterats på implementeringsnivå snarare än på ramnivå. Till exempel har vi bildgrupper som innehåller bilder. Det skulle ha varit enkelt för dessa grupper att hålla koll på sina barnbilder utan att det behövs ett generiskt räckvidd.
Vi bestämde oss slutligen för att skapa ett omfattande träningsområde av komponenter i dokumentet. Att ha scopes gör många saker enklare och tillåter oss att få mer generisk namngivning av komponenter, men de måste självklart hanteras. Om du förstör en komponent måste du ta bort den från dess överordnade omfattning. Om du flyttar en komponent måste den tas bort från en och läggas till en annan.
Omfattningen är en särskild hash eller ett kartobjekt, där varje av de barn som ingår i räckvidden är objektets egenskaper. Omfattningen, i Milo, finns på behållarens fasett, som i sig har väldigt liten funktionalitet. Omfattningsobjektet har dock en mängd olika metoder för att manipulera och iterera sig, men för att undvika namespace-konflikter heter alla dessa metoder med en understrykning i början.
var scope = myComponent.container.scope; scope._each (funktion (childComp) // iterera varje barnkomponent); // tillgång till en specifik komponent i omfattningen var testComp = scope.testComp; // få det totala antalet barnkomponenter var totalt = scope._length (); // lägg till en ny komponent inom ramen scope._add (newComp);
Vi ville ha lös koppling mellan komponenter, så vi bestämde oss för att ha meddelandefunktionalitet kopplad till alla komponenter och fasetter.
Den första implementeringen av budbäraren var bara en samling metoder som hanterade abonnentrader. Både metoderna och matrisen blandades direkt in i objektet som implementerade meddelanden.
En förenklad version av den första messenger-implementeringen ser något ut så här:
var messengerMixin = initMessenger: initMessenger, på: på, av: av, postMessage: postMessage; funktion initMessenger () this._subscribers = ; funktionen på (meddelande, abonnent) var msgSubscribers = this._subscribers [message] = this._subscribers [message] || []; om (msgSubscribers.indexOf (abonnent) == -1) msgSubscribers.push (abonnent); funktion av (meddelande, abonnent) var msgSubscribers = this._subscribers [message]; om (msgSubscribers) om (abonnent) _.spliceItem (msgSubscribers, abonnent); Annars radera this._subscribers [message]; funktion postMessage (meddelande, data) var msgSubscribers = this._subscribers [message]; om (msgSubscribers) msgSubscribers.forEach (funktion (abonnent) subscriber.call (detta, meddelande, data););
Alla objekt som använde denna blandning kan ha meddelanden som emitterats på det (av objektet själv eller med någon annan kod) med skicka meddelande
Metod och prenumerationer på denna kod kan slås på och av med metoder som har samma namn.
Numera har budbärare utvecklats väsentligt för att tillåta:
evenemang
facet använder den för att avslöja DOM-händelser via Milo messenger. Denna funktion är implementerad via en separat klass MessageSource
och dess underklasser.Data
facet använder den för att översätta ändringar och infoga DOM-händelser till dataförändringshändelser (se modeller nedan). Denna funktion är implementerad via en separat klass MessengerAPI och dess underklasser.component.on ('stateready', abonnent: func, context: context);
en gång
metodskicka meddelande
(vi betraktade variabelt antal argument i skicka meddelande
, men vi ville ha ett mer konsekvent meddelandehanterings API än vad vi skulle ha med variabla argument)Det huvudsakliga designfelet som vi gjorde när vi utvecklade budbäraren var att alla meddelanden skickades synkront. Eftersom JavaScript är enkelgängad kommer långa sekvenser av meddelanden med komplexa operationer som utförs enkelt att låsa upp gränssnittet. Att ändra Milo för att göra meddelandedisponering asynkron var lätt (alla abonnenter kallas på egna exekveringsblock med setTimeout (abonnent, 0)
, ändra resten av ramverket och applikationen var svårare - medan de flesta meddelanden kan skickas asynkront finns det många som fortfarande måste skickas synkront (många DOM-händelser som har data i dem eller platser där prevent
kallas). Som standard skickas meddelandena asynkront och det finns ett sätt att göra dem synkrona antingen när meddelandet skickas:
component.postMessageSync ("mymessage", data);
eller när prenumerationen skapas:
component.onSync ('mymessage', funktion (msg, data) // ...);
Ett annat designbeslut som vi gjorde var hur vi exponerade messenger metoder på objekten som använder dem. Ursprungligen blandades metoder helt enkelt i objektet, men vi tyckte inte att alla metoder är utsatta och vi kunde inte ha fristående budbärare. Så budbärare re-implementerades som en separat klass baserad på en abstrakt klass Mixin.
Mixin-klassen tillåter exponeringsmetoder för en klass på ett värdobjekt på sådant sätt att när metoder kallas kommer kontextet fortfarande att vara Mixin snarare än värdobjektet.
Det visade sig vara en mycket bekväm mekanism - vi kan ha full kontroll över vilka metoder som exponeras och ändra namnen efter behov. Det fick oss också att ha två budbärare på ett objekt som används för modeller.
Generellt visade sig Milo Messenger vara en mycket solid mjukvara som kan användas både i webbläsare och i Node.js. Det har härdats av användningen i vårt produktionshanteringssystem som har tiotusentals kodrader.
I nästa artikel kommer vi att titta på möjligen den mest användbara och komplexa delen av Milo. Milo-modellerna tillåter inte bara säker, djup tillgång till fastigheter utan även händelseabonnemang på förändringar på alla nivåer.
Vi undersöker också vår implementering av minder, och hur vi använder kopplingsobjekt för att göra en eller tvåvägsbindning av datakällor.
Observera att denna artikel skrevs av både Jason Green och Evgeny Poberezkin.