Rolling Your Own Framework

Å bygge et rammeverk fra bunnen av er ikke noe vi spesielt har bestemt oss for å gjøre. Du må være gal, ikke sant? Med den overflod av JavaScript-rammer der ute, hvilken mulig motivasjon kan vi ha for å rulle våre egne? 

Vi var opprinnelig på jakt etter et rammeverk for å bygge det nye innholdsstyringssystemet for The Daily Mail-nettsiden. Hovedmålet var å gjøre redigeringsprosessen mye mer interaktiv med alle elementene i en artikkel (bilder, embeds, utropsruter og så videre) å være draggable, modulære og selvstyrende.

Alle rammene som vi kunne ta hånd om var designet for mer eller mindre statisk brukergrensesnitt definert av utviklere. Vi trengte å lage en artikkel med både redigerbar tekst og dynamisk gjengitte brukergrensesnitt.

Ryggraden var for lavt nivå. Det gjorde lite mer enn å gi grunnleggende objekter struktur og meldingstjenester. Vi måtte bygge mye abstraksjon over Backbone-fundamentet, så vi bestemte oss for at vi helst ville bygge dette fundamentet selv.

AngularJS ble vår ramme for valg for å bygge små og mellomstore nettleserprogrammer som har relativt statiske brukergrensesnitt. Dessverre AngularJS er veldig mye en svart boks - det avslører ikke noe praktisk API for å utvide og manipulere objektene du lager med det - direktiver, kontrollører, tjenester. Mens AngularJS gir reaktive forbindelser mellom visninger og omfangsuttrykk, tillater det ikke å definere reaktive forbindelser mellom modeller, slik at enhver applikasjon av mellomstørrelse blir svært lik en jQuery-applikasjon med spaghetti av hendelselyttere og tilbakeringinger, med den eneste forskjellen som I stedet for hendelseslyttere har en kantet applikasjon seere og i stedet for å manipulere DOM manipulerer du scopes.

Det vi alltid ønsket, var et rammeverk som ville tillate;

  • Utvikle applikasjoner på en deklarativ måte med reaktive bindinger av modeller til synspunkter.
  • Opprette reaktive data bindinger mellom ulike modeller i applikasjonen for å håndtere dataformidling i en deklarativ snarere enn i en imperativ stil.
  • Sette inn validatorer og oversettere i disse bindingene, slik at vi kunne binde visninger til datamodeller i stedet for å se modeller som i AngularJS.
  • Nøyaktig kontroll over komponenter knyttet til DOM-elementer.
  • Fleksibilitet av visningsadministrasjon, slik at du både kan manipulere DOM-endringer automatisk og å gjenopprette noen seksjoner ved hjelp av en templerende motor i tilfeller der gjengivelse er mer effektiv enn DOM-manipulering.
  • Evne til å dynamisk opprette brukergrensesnitt.
  • Å kunne koble til mekanismer bak datareaktivitet og for å justere visningsoppdateringer og datastrøm.
  • Å kunne utvide funksjonaliteten til komponenter som leveres av rammen og å skape nye komponenter.

Vi kunne ikke finne det vi trengte i eksisterende løsninger, så vi begynte å utvikle Milo parallelt med applikasjonen som bruker den.

Hvorfor Milo?

Milo ble valgt som navnet på grunn av Milo Minderbinder, en krigsperspektiv fra Fang 22 av Joseph Heller. Etter å ha startet med å administrere rotoperasjoner, utvidet han dem til et lønnsomt handelsforetak som koblet alle sammen med alt, og at Milo og alle andre "har en andel".

Milo rammen har modulbinderen, som binder DOM-elementer til komponenter (via spesielle ml-bind attributt), og modulminneren som tillater etablering av levende reaktive forbindelser mellom forskjellige datakilder (modell og data faset av komponenter er slike datakilder).

Tilfeldigvis kan Milo leses som et akronym av MaIL Online, og uten det unike arbeidsmiljøet på Mail Online, ville vi aldri ha kunnet bygge den.

Administrere visninger

Binder

Visninger i Milo styres av komponenter, som i utgangspunktet forekommer av JavaScript-klasser, som er ansvarlige for å administrere et DOM-element. Mange rammer bruker komponenter som et konsept for å administrere UI-elementer, men den mest åpenbare som kommer til å tenke er Ext JS. Vi hadde jobbet mye med Ext JS (den gamle applikasjonen vi ble erstattet ble bygget med den), og ønsket å unngå det vi trodde å være to ulemper med sin tilnærming.

Den første er at Ext JS ikke gjør det enkelt for deg å administrere ditt oppslag. Den eneste måten å bygge et brukergrensesnitt på er å sette sammen nestede hierarkier av komponentkonfigurasjoner. Dette fører til unødvendig komplisert gjengitt markup og tar kontroll ut av utviklerens hender. Vi trengte en metode for å lage komponenter inline, i vår egen, håndlagde HTML-markup. Dette er hvor bindemiddel kommer inn.

Binder skanner vår oppslag på jakt etter ml-bind Tilordne slik at det kan instantiere komponenter og binde dem til elementet. Attributtet inneholder informasjon om komponentene; Dette kan inkludere komponentklassen, fasetter, og må inneholde komponentnavnet.

Vår milo komponent

Vi snakker om fasetter om et minutt, men for nå la oss se på hvordan vi kan ta denne attributtverdien og trekke ut konfigurasjonen fra den ved hjelp av et vanlig uttrykk.

Var bindAttrRegex = / ^ ([^ \: \ [\]] *) (?: \ [([^ \: \ [\]] *) \])? \:? ([^:] *) $ / ; var result = value.match (bindAttrRegex); // resultat er en matrise med // resultat [0] = 'ComponentClass [facet1, facet2]: componentName'; // resultat [1] = 'ComponentClass'; // resultat [2] = 'facet1, facet2'; // resultat [3] = 'komponentnavn';

Med den informasjonen er alt vi trenger å gjøre, iterere over alle ml-bind attributter, trekk ut disse verdiene, og opprett instanser for å administrere hvert element.

Var bindAttrRegex = / ^ ([^ \: \ [\]] *) (?: \ [([^ \: \ [\]] *) \])? \:? ([^:] *) $ / ; funksjonsbinder (tilbakeringing) var scope = ; // vi får alle elementene med ml-bind-attributtet var els = document.querySelectorAll ('[ml-bind]'); Array.prototype.forEach.call (els, funksjon (el) var attrText = el.getAttribute ('ml-bind'); varresultat = attrText.match (bindAttrRegex); var className = resultat [1] || 'Komponent '; var fasets = resultat [2] .split (', '); var compName = resultater [3]; // forutsatt at vi har et registerobjekt for alle våre klasser var comp = ny klasseRegistry [className] (el); comp .addFacets (fasetter); comp.name = compName; scope [compName] = comp; // vi holder en referanse til komponenten på elementet el .___ milo_component = comp;); tilbakeringing (omfang);  bindemiddel (funksjon (omfang) console.log (scope););

Så med bare en liten regex og litt DOM-kryssing, kan du lage ditt eget mini-rammeverk med egendefinert syntaks for å passe din forretningslogikk og kontekst. I svært liten kode har vi satt opp en arkitektur som muliggjør modulære, selvstyrende komponenter, som kan brukes uansett. Vi kan lage praktisk og declarative syntaks for instantiating og konfigurering av komponenter i HTML, men i motsetning til vinkel, kan vi administrere disse komponentene, men vi liker.

Ansvar-Driven Design

Den andre tingen vi ikke likte om Ext JS var at den har et veldig bratt og stivt klassehierarki, noe som ville ha gjort det vanskelig å organisere våre komponentklasser. Vi prøvde å skrive en liste over alle oppføringene som en gitt komponent i en artikkel kan ha. For eksempel kan en komponent redigeres, den kan høre på hendelser, det kan være et dråpemål eller være draget i seg selv. Dette er bare noen av de oppføringene som trengs. En foreløpig liste som vi skrev opp, hadde om lag 15 forskjellige typer funksjonalitet som kunne kreves av en bestemt komponent.

Å forsøke å organisere disse atferdene i en slags hierarkisk struktur ville ikke bare vært en stor hodepine, men også svært begrensende bør vi noen gang ønske å endre funksjonaliteten til en gitt komponentklasse (noe vi endte med å gjøre mye). Vi bestemte oss for å implementere et mer fleksibelt objektorientert designmønster.

Vi hadde lest om ansvar-drevet design, som i motsetning til den mer vanlige modellen for å definere oppførelsen av en klasse sammen med dataene den har, er mer opptatt av de handlingene som et objekt er ansvarlig for. Dette passet oss godt da vi hadde å gjøre med en kompleks og uforutsigbar datamodell, og denne tilnærmingen ville tillate oss å forlate implementeringen av disse detaljene til senere. 

Nøkkelen vi tok bort fra RDD var konseptet Roller. En rolle er et sett relatert ansvar. Når det gjelder vårt prosjekt, identifiserte vi roller som redigering, sleping, slippesone, valgbar eller hendelser blant mange andre. Men hvordan representerer du disse rollene i kode? For det lånte vi fra dekoratormønsteret.

Dekoratormønsteret gjør at oppførsel kan legges til et enkelt objekt, enten statisk eller dynamisk, uten å påvirke oppførselen til andre objekter fra samme klasse. Nå, mens run-time manipulering av klassen atferd ikke har vært spesielt nødvendig i dette prosjektet, var vi veldig interessert i den typen innkapsling denne ideen gir. Milos implementering er en slags hybrid som involverer objekter som kalles fasetter, som er knyttet som egenskaper til komponent-forekomsten. Fasaden får en referanse til komponenten, det er 'eier' og et konfigurasjonsobjekt, som gjør at vi kan tilpasse fasetter for hver komponentklasse. 

Du kan tenke på fasetter som avanserte, konfigurerbare mixins som får sitt eget navneområde på eierobjektet og til og med sine egne i det metode, som må overskrives av fasettunderklassen.

funksjon Facet (eier, config) this.name = this.constructor.name.toLowerCase (); this.owner = eier; this.config = config || ; this.init.apply (dette, argumenter);  Facet.prototype.init = funksjonen Facet $ init () ;

Så vi kan subclass denne enkle fasett klasse og skape spesifikke fasetter for hver type oppførsel som vi ønsker. Milo kommer prebuilt med en rekke fasetter, for eksempel DOM fasett, som gir en samling av DOM verktøy som opererer på eier komponentens element, og Liste og Punkt fasetter, som jobber sammen for å lage lister over gjentatte komponenter.

Disse fasettene blir da samlet sammen av det vi kalte en FacetedObject, som er en abstrakt klasse som alle komponenter arver fra. De FacetedObject har en klassemetode kalt createFacetedClass som bare subclasses seg, og fester alle fasetter til a fasetter eiendom på klassen. På den måten, når FacetedObject får instantiated, den har tilgang til alle sine faset klasser, og kan iterate dem å bootstrap komponenten.

funksjon FacetedObject (facetsOptions / *, andre init args * /) facetsOptions = facetsOptions? _.klone (facetsOptions): ; var thisClass = this.constructor, facets = ; hvis (! thisClass.prototype.facets) kaste ny feil ('Ingen fasetter definert'); _.eachKey (this.facets, instantiateFacet, dette, sant); Object.defineProperties (dette, fasetter); hvis (this.init) this.init.apply (dette, argumenter); funksjon instantiateFacet (facetClass, fct) var facetOpts = facetsOptions [fct]; slette facetsOptions [fct]; fasetter [fct] = tallverdig: falsk, verdi: ny facetClass (dette, facetOpts);  FacetedObject.createFacetedClass = funksjon (navn, fasetterClasses) var FacetedClass = _.createSubclass (dette, navnet, sant); _.extendProto (FacetedClass, facets: facetsClasses); returnere FacetedClass; ;

I Milo abstrakte vi litt videre ved å skape en base Komponent klasse med en matchende createComponentClass klassemetode, men grunnprinsippet er det samme. Med viktige oppføringer som styres av konfigurerbare fasetter, kan vi lage mange forskjellige komponentklasser i en deklarativ stil uten å måtte skrive for mye tilpasset kode. Her er et eksempel ved å bruke noen av de utelukkende fasene som følger med Milo.

Var Panel = Component.createComponentClass ('Panel', dom: cls: 'my-panel'en, tagName:' div ', hendelser: messages: ' click ': onPanelClick, dra: messages:  ..., slipp: messages: ..., container: undefined);

Her har vi opprettet en komponentklasse som heter Panel, som har tilgang til DOM-verktøymetoder, vil automatisk sette sin CSS-klasse på i det, det kan lytte til DOM hendelser og vil sette opp en klikkbehandler på i det, det kan bli trukket rundt, og også fungere som et dropmål. Den siste fasetten der, container sikrer at denne komponenten setter opp sitt eget omfang, og kan faktisk ha barnekomponenter.

omfang

Vi hadde omtalt om hvorvidt alle komponenter som er knyttet til dokumentet skal danne en flat struktur eller skal danne sitt eget tre, hvor barn bare er tilgjengelige fra foreldrene deres.

Vi ville definitivt ha behov for noen situasjoner, men det kunne ha blitt håndtert på implementeringsnivå, heller enn på rammebasis. For eksempel har vi bildegrupper som inneholder bilder. Det ville vært greit for disse gruppene å holde oversikt over sine barnbilder uten behov for et generisk omfang.

Vi bestemte oss endelig for å opprette et omfangsdel av komponenter i dokumentet. Å ha scopes gjør mange ting lettere og lar oss få mer generisk navngivning av komponenter, men de må selvfølgelig forvaltes. Hvis du ødelegger en komponent, må du fjerne den fra det overordnede omfanget. Hvis du flytter en komponent, må den fjernes fra en og legges til en annen.

Omfanget er et spesielt hash- eller kartobjekt, med hver av barna som er inneholdt i omfanget som egenskapene til objektet. Omfanget, i Milo, finnes på beholderfasetten, som i seg selv har svært lite funksjonalitet. Omfanget objektet, men har en rekke metoder for å manipulere og iterere seg selv, men for å unngå navneområde konflikter, er alle disse metodene navngitt med en understreke i begynnelsen.

var scope = myComponent.container.scope; scope._each (funksjon (childComp) // iterate hvert barn komponent); // tilgang til en bestemt komponent på omfanget var testComp = scope.testComp; // få totalt antall barnekomponenter var totalt = scope._length (); // legge til en ny komponent av omfanget scope._add (newComp);

Meldinger - Synkron vs Asynkron

Vi ønsket å ha løs kobling mellom komponenter, så vi bestemte oss for å ha meldingsfunksjonalitet knyttet til alle komponenter og fasetter.

Den første implementeringen av budbringeren var bare en samling metoder som ledet arrays av abonnenter. Både metodene og arrayet ble blandet rett inn i objektet som implementerte meldinger.

En forenklet versjon av den første messengerimplementasjonen ser noe ut som dette:

var messengerMixin = initMessenger: initMessenger, på: på, av: av, postMessage: postMessage; funksjon initMessenger () this._subscribers = ;  funksjon på (melding, abonnent) var msgSubscribers = this._subscribers [message] = this._subscribers [melding] || []; hvis (msgSubscribers.indexOf (abonnent) == -1) msgSubscribers.push (abonnent);  funksjon av (melding, abonnent) var msgSubscribers = this._subscribers [message]; hvis (msgSubscribers) hvis (abonnent) _.spliceItem (msgSubscribers, abonnent); ellers slett dette.subscribers [melding];  funksjon postMessage (melding, data) var msgSubscribers = this._subscribers [melding]; hvis (msgSubscribers) msgSubscribers.forEach (funksjon (abonnent) subscriber.call (dette, melding, data););  

Ethvert objekt som brukte denne blandingen, kan ha meldinger sendt ut på det (ved objektet selv eller med en annen kode) med postmessage Metode og abonnement på denne koden kan slås på og av med metoder som har de samme navnene.

I dag har messengers vesentlig utviklet seg for å tillate: 

  • Feste eksterne kilder til meldinger (DOM-meldinger, vindumeldinger, dataendringer, en annen messenger etc.) - f.eks. arrangementer Facet bruker den til å avsløre DOM-hendelser via Milo messenger. Denne funksjonaliteten er implementert via en egen klasse MessageSource og dens underklasser.
  • Definere tilpassede meldings-APIer som oversetter både meldinger og data om eksterne meldinger til intern melding. f.eks. Data facet bruker den til å oversette endring og innføre DOM hendelser til dataendringshendelser (se Modeller nedenfor). Denne funksjonaliteten er implementert via en separat klasse MessengerAPI og dens underklasser.
  • Mønsterabonnementer (ved bruk av vanlige uttrykk). F.eks modeller (se nedenfor) internt bruk mønsterabonnement for å tillate dype modellendringsabonnementer.
  • Definerer hvilken som helst kontekst (verdien av dette i abonnenten) som en del av abonnementet med denne syntaksen:
component.on ('stateready', abonnent: func, kontekst: kontekst);
  • Opprette abonnement som bare sendes en gang med en gang metode
  • Passerer tilbakeringing som en tredje parameter i postmessage (vi vurderte variabelt antall argumenter i postmessage, men vi ønsket en mer konsistent messaging API enn vi ville ha med variable argumenter)
  • etc.

Den største designfeilen vi gjorde mens du utviklet messenger var at alle meldinger ble sendt synkront. Siden JavaScript er single-threaded, vil lange sekvenser av meldinger med komplekse operasjoner utføres ganske enkelt låse brukergrensesnittet. Endring av Milo for å gjøre meldingsforsendelse asynkron var lett (alle abonnenter er kalt på sine egne eksekveringsblokker ved hjelp av setTimeout (abonnent, 0), endrer resten av rammen og applikasjonen var vanskeligere - mens de fleste meldinger kan sendes asynkront, er det mange som fortsatt må sendes synkront (mange DOM-hendelser som har data i dem eller steder der preventDefault er kalt). Som standard sendes meldinger asynkront, og det er en måte å gjøre dem synkron enten når meldingen sendes:

component.postMessageSync ('mymessage', data);

eller når abonnement er opprettet:

component.onSync ('mymessage', funksjon (msg, data) // ...); 

En annen designavgjørelse vi gjorde, var måten vi eksponerte på messengerens metoder på objektene som brukte dem. Opprinnelig ble metodene enkelt blandet inn i objektet, men vi likte ikke at alle metodene ble utsatt, og vi kunne ikke ha frittstående budbringere. Så budbringere ble reimplementert som en egen klasse basert på en abstrakt klasse Mixin. 

Mixin-klassen tillater eksponeringsmetoder for en klasse på et vertobjekt på en slik måte at når metoder blir kalt, vil konteksten fortsatt være Mixin i stedet for vertsobjektet.

Det viste seg å være en veldig praktisk mekanisme - vi kan ha full kontroll over hvilke metoder som blir utsatt og endre navnene etter behov. Det tillot oss også å ha to budbringere på ett objekt, som brukes til modeller.

Generelt viste Milo Messenger seg for å være et veldig solid stykke programvare som kan brukes alene, både i nettleseren og i Node.js. Det har blitt herdet av bruk i vårt produksjonsstyringssystem som har titusenvis kodelinjer.

Neste gang

I neste artikkel ser vi på muligens den mest nyttige og komplekse delen av Milo. Milo-modellene tillater ikke bare trygg, dyp tilgang til egenskaper, men også hendelsesabonnement på endringer på alle nivåer. 

Vi vil også undersøke implementeringen av minder, og hvordan vi bruker koblingsobjekter for å gjøre en eller toveisbinding av datakilder.

Merk at denne artikkelen ble skrevet av både Jason Green og Evgeny Poberezkin.