Noen gang undret seg over Mootools magi? Noen gang lurt på hvordan Dojo gjør det? Har du noen gang vært nysgjerrig på jQuery's gymnastikk? I denne opplæringen skal vi snike bak kulissene og prøve vår hånd på å bygge en super enkel versjon av favorittbiblioteket ditt.
Vi bruker JavaScript-biblioteker nesten hver dag. Når du bare er i gang, har du noe som jQuery, fantastisk, hovedsakelig på grunn av DOM. For det første kan DOM være ganske grovt å skremme for en nybegynner; Det er en ganske dårlig unnskyldning for en API. For det andre er det ikke engang konsekvent i alle nettlesere.
Vi pakker elementene i et objekt fordi vi vil kunne lage metoder for objektet.
I denne opplæringen skal vi ta en (bestemt lavt) stikk ved å bygge en av disse bibliotekene fra bunnen av. Ja, det blir morsomt, men før du blir for opptatt, la meg klargjøre noen få punkter:
føyer
og foranstilt
metoder vil bare fungere hvis du sender dem en forekomst av vårt bibliotek; de vil ikke fungere med rå DOM noder eller nodelists.En ting til og med: Mens vi ikke skal skrive tester for dette biblioteket, gjorde jeg det da jeg først utviklet dette. Du kan få biblioteket og tester på Github.
Vi starter med en pakkekode som inneholder hele biblioteket vårt. Det er ditt typiske umiddelbart påkalte funksjonsuttrykk (IIFE).
window.dome = (function () funksjon Dome (els) var dome = get: funksjon (selector) ; return dome; ());
Som du kan se, ringer vi til biblioteket Dome, fordi det hovedsakelig er et DOM-bibliotek. Ja, det er halt.
Vi har et par ting som skjer her. Først har vi en funksjon; Det vil etter hvert bli en konstruktørfunksjon for forekomster av vårt bibliotek; disse objektene vil pakke inn våre valgte eller opprettede elementer.
Da har vi vår kuppel
objekt, som er vårt faktiske bibliotek objekt Som du kan se, er den returnert på slutten der. Den er tom få
funksjon, som vi skal bruke for å velge elementer fra siden. Så la oss fylle det inn nå.
De dome.get
funksjonen vil ta en parameter, men det kan være en rekke ting. Hvis det er en streng, antar vi at det er en CSS-väljare; men vi kan også ta en enkelt DOM Node, eller en NodeList.
få: funksjon (selector) var els; hvis (typevelger === "streng") els = document.querySelectorAll (selector); annet hvis (selector.length) els = selector; ellers els = [selector]; returner ny Dome (els);
Vi bruker document.querySelectorAll
for å forenkle funn av elementer: selvfølgelig begrenser dette nettleserens støtte, men i dette tilfellet er det greit. Hvis velger
er ikke en streng, vi skal sjekke for en lengde
eiendom. Hvis det eksisterer, vet vi at vi har en NodeList
; ellers har vi et enkelt element, og vi legger det i en matrise. Det er fordi vi trenger en matrise som skal overføres til vårt kall til kuppel
på bunnen der; Som du ser, returnerer vi en ny kuppel
gjenstand. Så la oss gå tilbake til det tomme kuppel
funksjonen og fyll den inn.
kuppel
forekomsterHer er det kuppel
funksjon:
funksjon Dome (els) for (var i = 0; i < els.length; i++ ) this[i] = els[i]; this.length = els.length;
Jeg anbefaler deg virkelig å grave rundt i noen av favorittbibliotekene dine.
Dette er veldig enkelt: vi bare gjenkjenner de elementene vi valgte og holder dem på det nye objektet med numeriske indekser. Deretter legger vi til en lengde
eiendom.
Men hva er poenget her? Hvorfor ikke bare returnere elementene? Vi pakker elementene i et objekt fordi vi vil kunne lage metoder for objektet; Dette er metodene som gjør at vi kan samhandle med disse elementene. Dette er faktisk en kokt ned versjon av måten jQuery gjør det på.
Så nå har vi vår kuppel
objektet blir returnert, la oss legge til noen metoder for prototypen. Jeg skal sette disse metodene rett under kuppel
funksjon.
De første funksjonene vi skal skrive er enkle bruksfunksjoner. Siden vår kuppel
objekter kan pakke inn mer enn ett DOM-element, vi må sløyfe over hvert element i stort sett hver metode; så disse verktøyene vil være nyttig.
La oss starte med en kart
funksjon:
Dome.prototype.map = funksjon (tilbakeringing) var results = [], i = 0; for (; < this.length; i++) results.push(callback.call(this, this[i], i)); return results; ;
Selvfølgelig, den kart
funksjonen tar en enkelt parameter, en tilbakeringingsfunksjon. Vi slår over elementene i matrisen, samler hva som returneres fra tilbakeringingen i resultater
array. Legg merke til hvordan vi ringer den tilbakeringingsfunksjonen:
callback.call (dette, dette [i], i));
Ved å gjøre det på denne måten, blir funksjonen kalt i sammenheng med vår kuppel
eksempel, og det vil motta to parametre: det nåværende elementet og indeksnummeret.
Vi vil også ha en for hver
funksjon. Dette er faktisk veldig enkelt:
Dome.prototype.forEach (tilbakeringing) this.map (tilbakeringing); returnere dette; ;
Siden den eneste forskjellen mellom kart
og for hver
er det kart
trenger å returnere noe, vi kan bare sende vår tilbakeringing til this.map
og ignorere den returnerte gruppen; I stedet kommer vi tilbake dette
for å gjøre vårt bibliotek kjedelig. Vi skal bruke for hver
ganske mye. Så vær oppmerksom på at når vi kommer tilbake this.forEach
ring fra en funksjon, vi returnerer faktisk dette
. For eksempel returnerer disse metodene det samme:
Dome.prototype.someMethod1 = funksjon (tilbakeringing) this.forEach (tilbakeringing); returnere dette; ; Dome.prototype.someMethod2 = funksjon (tilbakeringing) return dette.forEach (tilbakeringing); ;
En til: mapOne
. Det er lett å se hva denne funksjonen gjør, men det virkelige spørsmålet er, hvorfor trenger vi det? Dette krever litt av det du kan kalle "bibliotekets filosofi."
For det første kan DOM være ganske grovt å vrangle for en nybegynner; Det er en ganske dårlig unnskyldning for en API.
Hvis du bygger et bibliotek, var det bare å skrive koden, ville det ikke være for vanskelig en jobb. Men da jeg jobbet med dette prosjektet, fant jeg at det tøffere var å avgjøre hvordan bestemte metoder skulle fungere.
Snart skal vi bygge en tekst
metode som returnerer teksten til de valgte elementene. Hvis vår kuppel
objektet omsluttes flere DOM-noder (dome.get ( "LI")
, for eksempel), hva skal dette returnere? Hvis du gjør noe lignende i jQuery ($ ( "Li"). Tekst ()
), får du en enkelt streng med teksten til alle elementene sammenkoblet sammen. Er dette nyttig? Jeg tror ikke det, men jeg er ikke sikker på hva en bedre returverdi ville være.
For dette prosjektet returnerer jeg teksten til flere elementer som en matrise, med mindre det bare er ett element i matrisen; da returnerer vi bare tekststrengen, ikke en matrise med et enkelt element. Jeg tror du oftest får teksten til et enkelt element, så vi optimaliserer for det tilfellet. Men hvis du får teksten til flere elementer, returnerer vi noe du kan jobbe med.
Så mapOne
Metoden vil bare løpe kart
, og returner deretter arrayet eller det enkelte elementet som var i arrayet. Hvis du fortsatt ikke er sikker på hvordan dette er nyttig, hold deg fast: du vil se!
Dome.prototype.mapOne = funksjon (tilbakeringing) var m = this.map (tilbakeringing); retur m.length> 1? m: m [0]; ;
Deretter legger vi til det tekst
metode. På samme måte som jQuery, kan vi sende den en streng og sette elementets tekst, eller bruk ingen parametere for å få teksten tilbake.
Dome.prototype.text = funksjon (tekst) if (type av tekst! == "undefined") return this.forEach (funksjon (el) el.innerText = text;); annet return this.mapOne (funksjon (el) return el.innerText;); ;
Som du kanskje forventer, må vi sjekke for en verdi i tekst
for å se om vi setter inn eller får. Merk at bare hvis (tekst)
ville ikke fungere, fordi en tom streng er en falsk verdi.
Hvis vi setter inn, gjør vi en for hver
over elementene og sett deres innertext
eiendom til tekst
. Hvis vi får, returnerer vi elementene ' innertext
eiendom. Legg merke til vår bruk av mapOne
metode: hvis vi jobber med flere elementer, vil dette returnere en matrise; ellers vil det bare være strengen.
De html
Metoden vil gjøre stort sett det samme som tekst
, bortsett fra at det vil bruke innerhtml
eiendom, i stedet for innertext
.
Dome.prototype.html = funksjon (html) if (typeof html! == "undefined") this.forEach (funksjon (el) el.innerHTML = html;); returnere dette; annet return this.mapOne (funksjon (el) return el.innerHTML;); ;
Som jeg sa: nesten identisk.
Neste opp vil vi kunne legge til og fjerne klasser; så la oss skrive addClass
og removeClass
fremgangsmåter.
Våre addClass
Metoden vil ta enten en streng eller en rekke klassenavn. For å gjøre dette arbeidet må vi kontrollere typen av parameteren. Hvis det er en matrise, slår vi over det og lager en rekke klassenavn. Ellers vil vi bare legge til et enkelt mellomrom på forsiden av klassenavnet, så det roter ikke med de eksisterende klassene på elementet. Så slår vi bare over elementene og legger til de nye klassene til klassenavn
eiendom.
Dome.prototype.addClass = funksjon (klasser) var className = ""; hvis (type av klasser! == "streng") for (var i = 0; i < classes.length; i++) className += " " + classes[i]; else className = " " + classes; return this.forEach(function (el) el.className += className; ); ;
Ganske enkelt, eh?
Nå, hva med å fjerne klasser? For å holde det enkelt, tillater vi bare å fjerne en klasse av gangen.
Dome.prototype.removeClass = funksjon (clazz) return this.forEach (funksjon (el) var cs = el.className.split (""), i; mens ((i = cs.indexOf (clazz))> 1) cs = cs.slice (0, i) .concat (cs.slice (++ i)); el.className = cs.join ("");); ;
På hvert element deler vi el.className
inn i en matrise. Deretter bruker vi en stundsløyfe til å skille ut den avskyelige klassen til cs.indexOf (Clazz)
returnerer -1. Vi gjør dette for å dekke kantsaken der de samme klassene har blitt lagt til et element mer enn en gang: vi må sørge for at det virkelig er borte. Når vi er sikre på at vi har kuttet ut alle forekomster av klassen, går vi sammen med matrisen med mellomrom og setter den på el.className
.
Den verste nettleseren vi jobber med er IE8. I vårt lille bibliotek er det bare en IE-feil som vi trenger å håndtere; Heldigvis er det ganske enkelt. IE8 støtter ikke Array
metode oversikt over
; vi bruker den inn removeClass
, så la oss polyfill det:
hvis (type av Array.prototype.indexOf! == "funksjon") Array.prototype.indexOf = funksjon (element) for (var i = 0; i < this.length; i++) if (this[i] === item) return i; return -1; ;
Det er ganske enkelt, og det er ikke en full implementering (støtter ikke den andre parameteren), men det vil fungere for våre formål.
Nå vil vi ha en attr
funksjon. Dette vil være enkelt, fordi det er praktisk talt identisk med vår tekst
eller html
metoder. Som disse metodene, kan vi både få og sette attributter: vi tar et attributtnavn og verdi som skal settes, og bare et attributtnavn for å få.
Dome.prototype.attr = funksjon (attr, val) hvis (type av val! == "undefined") return this.forEach (funksjon (el) el.setAttribute (attr, val);); annet return this.mapOne (funksjon (el) return el.getAttribute (attr);); ;
Hvis val
har en verdi, vi slår gjennom elementene og setter det valgte attributtet med den verdien, ved hjelp av elementets setAttribute
metode. Ellers vil vi bruke mapOne
å returnere attributtet via getAttribute
metode.
Vi bør kunne lage nye elementer, som alle gode bibliotekskanaler. Selvfølgelig vil dette ikke være bra som en metode på en kuppel
eksempel, så la oss sette det rett på vår kuppel
gjenstand.
var dome = // få metode her opprette: funksjon (tagnavn, attrs) ;
Som du kan se, tar vi to parametere: navnet på elementet og et objekt av attributter. De fleste attributter blir brukt via vår attr
metode, men to vil få spesiell behandling. Vi bruker addClass
metode for klassenavn
eiendom og tekst
metode for tekst
eiendom. Selvfølgelig må vi opprette elementet og kuppel
objekt først. Her er alt som skjer:
opprett: funksjon (tagnavn, attrs) var el = ny Dome ([document.createElement (tagName)]); hvis (attrs) hvis (attrs.className) el.addClass (attrs.className); slett attrs.className; hvis (attrs.text) el.text (attrs.text); slette attrs.text; for (var nøkkel i attrs) hvis (attrs.hasOwnProperty (nøkkel)) el.attr (nøkkel, attrs [nøkkel]); retur el;
Som du kan se, lager vi elementet og sender det til et nytt kuppel
gjenstand. Deretter behandler vi attributter. Legg merke til at vi må slette klassenavn
og tekst
attributter etter å ha jobbet med dem. Dette forhindrer at de blir brukt som attributter når vi slår over resten av nøklene i attrs
. Selvfølgelig slutter vi ved å returnere den nye kuppel
gjenstand.
Men nå som vi lager nye elementer, vil vi sette dem inn i DOM, rett?
Neste opp skriver vi føyer
og foranstilt
metoder, nå, disse er faktisk litt vanskelige funksjoner å skrive, hovedsakelig på grunn av flere bruk tilfeller. Her er det vi ønsker å kunne gjøre:
dome1.append (dome2); dome1.prepend (dome2);
Den verste nettleseren vi jobber med er IE8.
Brukstilfellene er som disse: Vi vil kanskje legge til eller prepend
Merk: Jeg bruker "ny" for å bety elementer som ikke er i DOM; eksisterende elementer er allerede i DOM.
La oss gå om det nå:
Dome.prototype.append = funksjon (els) this.forEach (funksjon (parEl, i) els.forEach (funksjon (childEl) );); ;
Vi forventer det els
parameter for å være a kuppel
gjenstand. Et komplett DOM-bibliotek vil akseptere dette som en knutepunkt eller en nodelist, men det vil vi ikke gjøre. Vi må sløyfe over hver av elementene våre, og så inne i det, løper vi over hver av elementene vi vil legge til.
Hvis vi legger til els
til mer enn ett element, må vi klone dem. Vi vil imidlertid ikke klone noder første gang de er vedlagt, bare etterfølgende tider. Så gjør vi dette:
hvis (i> 0) childEl = childEl.cloneNode (true);
At Jeg
kommer fra ytre for hver
loop: det er indeksen for det nåværende foreldreelementet. Hvis vi ikke legger til det første foreldreelementet, kloner vi noden. På den måten går den faktiske noden i den første overordnede noden, og alle andre foreldre vil få en kopi. Dette fungerer bra, fordi kuppel
objekt som ble sendt inn som argument vil bare ha de opprinnelige (uncloned) noder. Så, hvis vi bare legger til et enkelt element til et enkelt element, vil alle involverte noder være en del av deres respektive kuppel
objekter.
Til slutt vil vi faktisk legge til elementet:
parEl.appendChild (childEl);
Så, alt dette, er dette hva vi har:
Dome.prototype.append = funksjon (els) return this.forEach (funksjon (parEl, i) els.forEach (funksjon (childEl) hvis (i> 0) childEl = childEl.cloneNode (true); parEl .appendChild (childEl););); ;
foranstilt
MetodeVi ønsker å dekke de samme sakene for foranstilt
metode, så metoden er ganske veldig lik:
Dome.prototype.prepend = funksjon (els) return this.forEach (funksjon (parEl, i) for (var j = els.length -1; j> -1; j--) childEl = (i> 0 )? els [j] .cloneNode (true): els [j]; parEl.insertBefore (childEl, parEl.firstChild);); ;
Det forskjellige når det legges til forutsetning er at hvis du sekvensielt prepend en liste over elementer til et annet element, vil de ende opp i omvendt rekkefølge. Siden vi ikke kan for hver
bakover, jeg går gjennom løkken bakover med a til
sløyfe. Igjen kloner vi noden hvis dette ikke er den første forelder vi legger til.
For vår siste nodepåvirkningsmetode ønsker vi å kunne fjerne noder fra DOM. Lett, virkelig:
Dome.prototype.remove = function () return this.forEach (funksjon (el) return el.parentNode.removeChild (el);); ;
Bare gjenta gjennom noder og ring til removeChild
metode på hvert element er parentNode
. Skjønnheten her (alt takk til DOM) er at dette kuppel
objektet vil fortsatt fungere fint; Vi kan bruke hvilken som helst metode vi ønsker på den, inkludert å legge til eller prependere den tilbake til DOM. Hyggelig, eh?
Sist, men absolutt ikke minst, skal vi skrive noen funksjoner for hendelseshåndterere.
Som du sikkert vet, bruker IE8 de gamle IE-hendelsene, så vi må sjekke det. Også, vi kaster inn DOM 0 hendelsene, bare fordi vi kan.
Sjekk ut metoden, og så diskuterer vi det:
Dome.prototype.on = (function () if (document.addEventListener) returfunksjon (evt, fn) return this.forEach (funksjon (el) el.addEventListener (evt, fn, false);); ; else if (document.attachEvent) returfunksjon (evt, fn) return this.forEach (funksjon (el) el.attachEvent ("on" + evt, fn););; else returfunksjon (evt, fn) return this.forEach (funksjon (el) el ["on" + evt] = fn;);; ());
Her har vi en IIFE, og inne i det gjør vi funksjonskontroll. Hvis document.addEventListener
eksisterer, bruker vi det; Ellers vil vi sjekke for document.attachEvent
eller fall tilbake til DOM 0 hendelser. Legg merke til hvordan vi returnerer den endelige funksjonen fra IIFE: det er det som til slutt blir tildelt Dome.prototype.on
. Ved gjenkjenning av funksjon er det veldig praktisk å kunne tilordne den aktuelle funksjonen som dette, i stedet for å sjekke funksjonene hver gang funksjonen kjøres.
De av
funksjonen, som avhenger hendelseshåndterer, er ganske mye identisk:
Dome.prototype.off = (funksjon () if (document.removeEventListener) returfunksjon (evt, fn) return this.forEach (funksjon (el) el.removeEventListener (evt, fn, false);); ; else if (document.detachEvent) returfunksjon (evt, fn) return this.forEach (funksjon (el) el.detachEvent ("on" + evt, fn););; else returfunksjon (evt, fn) return this.forEach (funksjon (el) el ["på" + evt] = null;); ());
Jeg håper du gir vårt lille bibliotek et forsøk, og kanskje til og med utvide det litt. Som jeg nevnte tidligere, har jeg det på Github, sammen med Jasmine-testpakken for koden vi er skrevet over. Ta gjerne gaffel, spill rundt og send en trekkforespørsel.
La meg klargjøre igjen: poenget med denne opplæringen er ikke å foreslå at du alltid skal skrive egne biblioteker.
Det er dedikerte lag av mennesker som jobber sammen for å gjøre de store, etablerte bibliotekene så gode som mulig. Poenget her var å gi en liten titt på hva som kunne gå på innsiden av et bibliotek; Jeg håper du har hentet noen tips her.
Jeg anbefaler deg virkelig å grave rundt i noen av favorittbibliotekene dine. Du finner at de ikke er så kryptiske som du kanskje har trodd, og du vil sikkert lære mye. Her er noen gode steder å starte: