Bygg ditt første JavaScript-bibliotek

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:

  • Dette vil ikke være et fullverdig bibliotek. Åh, vi har et solidt sett med metoder for å skrive, men det er ingen jQuery. Vi gjør nok for å gi deg en god følelse for de slags problemer som du vil komme inn i når du bygger biblioteker.
  • Vi går ikke for komplett nettleserkompatibilitet over hele linjen her. Det vi skriver i dag, skal fungere på Internet Explorer 8+, Firefox 5+, Opera 10+, Chrome og Safari.
  • Vi skal ikke dekke alle mulige bruksområder av biblioteket vårt. For eksempel, vår 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.


Trinn 1: Opprette Library Boilerplate

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 funksjon, som vi skal bruke for å velge elementer fra siden. Så la oss fylle det inn nå.


Trinn 2: Få elementer

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.


Trinn 3: Opprette kuppel forekomster

Her 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.


Trinn 4: Legge til noen få verktøy

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."

En kort "filosofisk" omvei

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.

Tilbake til koding

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]; ;

Trinn 5: Arbeide med tekst og HTML

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.


Trinn 6: Hacking Classes

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.


Trinn 7: Å fikse en IE-feil

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.


Trinn 8: Justering av attributter

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.


Trinn 9: Opprette elementer

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?


Trinn 10: Tilføye og forberede elementer

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

  • ett nytt element til en eller flere eksisterende elementer.
  • flere nye elementer til ett eller flere eksisterende elementer.
  • ett eksisterende element til en eller flere eksisterende elementer.
  • flere eksisterende elementer til en eller flere eksisterende elementer.

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););); ;

De foranstilt Metode

Vi ø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.


Trinn 11: Fjerne nodene

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?


Trinn 12: Arbeide med hendelser

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;); ());

Det er det!

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:

  • 10 ting jeg lærte fra jQuery-kilden (av Paul Irish)
  • 11 Flere ting jeg lærte fra jQuery-kilden (også av Paul Irish)
  • Under jQuery's Bonnet (av James Padolsey)
  • Backbone.js: Hackers Guide, del 1, del 2, del 3, del 4
  • Kjenner du noen andre gode sammenbrudd i biblioteket? La oss se dem i kommentarene!