Ooit verwonderd over de magie van Mootools? Ooit afgevraagd hoe Dojo het doet? Ooit nieuwsgierig geweest naar de gymnastiek van jQuery? In deze tutorial gaan we achter de schermen sluipen en proberen we een supereenvoudige versie van je favoriete bibliotheek te maken.
We gebruiken bijna elke dag JavaScript-bibliotheken. Als je net begint, is iets als jQuery fantastisch, voornamelijk vanwege de DOM. Ten eerste kan de DOM nogal ruig zijn om te ruziën voor een beginner; het is een vrij slecht excuus voor een API. Ten tweede is het niet eens consistent in alle browsers.
We verpakken de elementen in een object omdat we methoden voor het object willen maken.
In deze tutorial gaan we een (beslist oppervlakkige) poging doen om een van deze bibliotheken helemaal opnieuw te bouwen. Ja, het zal leuk zijn, maar voordat je te opgewonden raakt, wil ik een paar dingen verduidelijken:
toevoegen
en prepend
methoden zullen alleen werken als u ze een exemplaar van onze bibliotheek geeft; ze zullen niet werken met onbewerkte DOM-knooppunten of -knooplijsten.Nog een ding: hoewel we geen tests voor deze bibliotheek zullen schrijven, deed ik dat toen ik dit voor het eerst ontwikkelde. U kunt de bibliotheek en tests op Github downloaden.
We beginnen met een wrapper-code die onze hele bibliotheek bevat. Het is je typische onmiddellijk opgeroepen functie-expressie (IIFE).
window.dome = (function () function Dome (els) var dome = get: function (selector) ; return dome; ());
Zoals je ziet, bellen we onze bibliotheek Dome, omdat het voornamelijk een DOM-bibliotheek is. Ja, het is zwak.
We hebben hier een paar dingen aan de hand. Ten eerste hebben we een functie; het zal uiteindelijk een constructorfunctie zijn voor de instanties van onze bibliotheek; die objecten zullen onze geselecteerde of gemaakte elementen omwikkelen.
Dan hebben we onze koepel
object, dat is ons eigenlijke bibliotheekobject; zoals je kunt zien, is het daar aan het einde terug. Het is leeg krijgen
functie, die we zullen gebruiken om elementen van de pagina te selecteren. Dus laten we dat nu invullen.
De dome.get
functie neemt één parameter, maar het kan een aantal dingen zijn. Als het een tekenreeks is, nemen we aan dat het een CSS-selector is; maar we kunnen ook een enkel DOM-knooppunt of een NodeList nemen.
get: function (selector) var els; if (type van selector === "string") els = document.querySelectorAll (selector); else if (selector.length) els = selector; else els = [selector]; retourneer nieuwe Dome (els);
We gebruiken document.querySelectorAll
om het vinden van elementen te vereenvoudigen: dit beperkt natuurlijk de ondersteuning van onze browser, maar in dit geval is dat goed. Als keuzeschakelaar
is geen string, we zullen controleren op een lengte
eigendom. Als het bestaat, weten we dat we een hebben nodelist
; anders hebben we een enkel element en we zullen dat in een array plaatsen. Dat komt omdat we een array nodig hebben om door te geven aan onze oproep Koepel
onderaan daar; zoals je kunt zien, geven we een nieuwe terug Koepel
voorwerp. Dus laten we teruggaan naar dat lege Koepel
functie en vul deze in.
Koepel
instantiesHier is dat Koepel
functie:
function Dome (els) for (var i = 0; i < els.length; i++ ) this[i] = els[i]; this.length = els.length;
Ik raad je echt aan om een paar van je favoriete bibliotheken te verkennen.
Dit is heel eenvoudig: we herhalen alleen de elementen die we hebben geselecteerd en plakken deze op het nieuwe object met numerieke indices. Vervolgens voegen we een toe lengte
eigendom.
Maar waar gaat het hier om? Waarom niet alleen de elementen retourneren? We verpakken de elementen in een object omdat we methoden voor het object willen maken; dit zijn de methoden die ons in staat stellen om met die elementen in wisselwerking te treden. Dit is eigenlijk een ingekorte versie van de manier waarop jQuery het doet.
Dus nu we onze hebben Koepel
object dat wordt geretourneerd, laten we enkele methoden toevoegen aan zijn prototype. Ik ga die methoden precies onder de Koepel
functie.
De eerste functies die we gaan schrijven, zijn eenvoudige hulpprogramma-functies. Sinds onze Koepel
objecten kunnen meer dan één DOM-element bevatten, we zullen in vrijwel elke methode over elk element moeten lopen; dus deze hulpprogramma's zijn handig.
Laten we beginnen met een kaart
functie:
Dome.prototype.map = function (callback) var results = [], i = 0; voor (; < this.length; i++) results.push(callback.call(this, this[i], i)); return results; ;
Natuurlijk, de kaart
functie neemt een enkele parameter, een callback-functie. We zullen lus over de items in de array, het verzamelen van wat wordt teruggegeven van de callback in de uitslagen
matrix. Merk op hoe we die callback-functie aanroepen:
callback.call (dit, dit [i], i));
Door het op deze manier te doen, zal de functie worden aangeroepen in de context van onze Koepel
Bijvoorbeeld, en het ontvangt twee parameters: het huidige element en het indexnummer.
We willen ook een forEach
functie. Dit is eigenlijk heel simpel:
Dome.prototype.forEach (callback) this.map (callback); geef dit terug; ;
Aangezien het enige verschil tussen kaart
en forEach
is dat kaart
moet iets retourneren, we kunnen gewoon onze callback doorgeven this.map
en negeer de geretourneerde array; in plaats daarvan komen we terug deze
om onze bibliotheek ketenbaar te maken. We zullen gebruiken forEach
best wel. Dus merk op dat wanneer we onze this.forEach
bellen vanuit een functie, we komen eigenlijk terug deze
. Deze methoden retourneren bijvoorbeeld feitelijk hetzelfde:
Dome.prototype.someMethod1 = function (callback) this.forEach (callback); geef dit terug; ; Dome.prototype.someMethod2 = function (callback) retourneer this.forEach (callback); ;
Nog een: mapOne
. Het is gemakkelijk om te zien wat deze functie doet, maar de echte vraag is, waarom hebben we het nodig? Dit vereist een beetje van wat je zou kunnen noemen "bibliotheekfilosofie."
Ten eerste kan de DOM nogal ruig zijn om te ruziën voor een beginner; het is een vrij slecht excuus voor een API.
Als het bouwen van een bibliotheek zo ongeveer de code zou schrijven, zou het niet zo moeilijk zijn om een baan te vinden. Maar toen ik aan dit project werkte, merkte ik dat het moeilijker was om te beslissen hoe bepaalde methoden zouden moeten werken.
Binnenkort gaan we een bouwen tekst
methode die de tekst van onze geselecteerde elementen retourneert. Als onze Koepel
object wikkelt meerdere DOM-knooppunten (dome.get ( "li")
, bijvoorbeeld), wat zou dit moeten teruggeven? Als je iets soortgelijks in jQuery doet ($ ( "Li"). Tekst ()
), krijg je een enkele string met de tekst van alle elementen samengevoegd. Is dit nuttig? Ik denk het niet, maar ik weet niet zeker wat een betere rendementswaarde zou zijn.
Voor dit project zal ik de tekst van meerdere elementen als een array retourneren, tenzij er maar één item in de array is; dan geven we alleen de tekststring terug, niet een array met een enkel item. Ik denk dat je meestal de tekst van een enkel element krijgt, dus we optimaliseren voor dat geval. Als u echter de tekst van meerdere elementen ontvangt, retourneren we iets waarmee u kunt werken.
Dus de mapOne
methode zal gewoon worden uitgevoerd kaart
, en vervolgens de array of het enkele item dat zich in de array bevond, retourneren. Als je nog steeds niet zeker weet hoe dit nuttig is, blijf dan rond: je zult het zien!
Dome.prototype.mapOne = function (callback) var m = this.map (callback); return m.length> 1? m: m [0]; ;
Laten we dat vervolgens toevoegen tekst
methode. Net als jQuery kunnen we het een reeks doorgeven en de tekst van het element instellen, of geen parameters gebruiken om de tekst terug te krijgen.
Dome.prototype.text = function (text) if (typeof!! == "undefined") return this.forEach (function (el) el.innerText = text;); else return this.mapOne (function (el) return el.innerText;); ;
Zoals je zou verwachten, moeten we controleren op een waarde in tekst
om te zien of we aan het werken zijn of krijgen. Merk op dat alleen als (tekst)
zou niet werken, omdat een lege string een valse waarde is.
Als we aan het werken zijn, doen we het forEach
over de elementen en stel hun in innerText
eigendom van de tekst
. Als we het krijgen, geven we de elementen terug innerText
eigendom. Let op ons gebruik van de mapOne
methode: als we met meerdere elementen werken, retourneert dit een array; anders zal het alleen de string zijn.
De html
methode zal vrijwel hetzelfde doen als tekst
, behalve dat het de innerHTML
eigendom, in plaats van innerText
.
Dome.prototype.html = function (html) if (typeof html! == "undefined") this.forEach (function (el) el.innerHTML = html;); geef dit terug; else return this.mapOne (function (el) return el.innerHTML;); ;
Zoals ik al zei: bijna identiek.
Vervolgens willen we klassen kunnen toevoegen en verwijderen; dus laten we de addClass
en removeClass
methoden.
Onze addClass
methode neemt een tekenreeks of een reeks klassenamen. Om dit te laten werken, moeten we het type van die parameter controleren. Als het een array is, zullen we eroverheen lopen en een reeks klassenamen maken. Anders voegen we gewoon een enkele spatie toe aan de voorkant van de klassenaam, zodat deze niet knoeit met de bestaande klassen in het element. Vervolgens lussen we gewoon over de elementen en voegen we de nieuwe klassen toe aan de naam van de klasse
eigendom.
Dome.prototype.addClass = function (classes) var className = ""; if (typeof! == "string") for (var i = 0; i < classes.length; i++) className += " " + classes[i]; else className = " " + classes; return this.forEach(function (el) el.className += className; ); ;
Vrij eenvoudig, hè?
Nu, hoe zit het met het verwijderen van klassen? Om het simpel te houden, mogen we slechts één les tegelijk verwijderen.
Dome.prototype.removeClass = function (clazz) return this.forEach (functie (el) var cs = el.className.split (""), i; while ((i = cs.indexOf (clazz))> - 1) cs = cs.slice (0, i) .concat (cs.slice (++ i)); el.className = cs.join ("");); ;
Op elk element splitsen we het el.className
in een array. Vervolgens gebruiken we een while-lus om de overtredende klasse tot rust te brengen cs.indexOf (Clazz)
levert -1 op. We doen dit om de edge-case te behandelen waarbij dezelfde klassen meerdere keren aan een element zijn toegevoegd: we moeten ervoor zorgen dat het echt weg is. Zodra we zeker weten dat we elke instantie van de klasse hebben verwijderd, verbinden we de array met spaties en zetten deze op el.className
.
De slechtste browser die we behandelen is IE8. In onze kleine bibliotheek is er maar één IE-bug waarmee we te maken hebben; Gelukkig is het vrij eenvoudig. IE8 ondersteunt de reeks
methode index van
; we gebruiken het removeClass
, dus laten we het polyfill:
if (typeof Array.prototype.indexOf! == "function") Array.prototype.indexOf = function (item) for (var i = 0; i < this.length; i++) if (this[i] === item) return i; return -1; ;
Het is vrij eenvoudig en het is geen volledige implementatie (ondersteunt de tweede parameter niet), maar het werkt voor onze doeleinden.
Nu willen we een attr
functie. Dit zal gemakkelijk zijn, omdat het praktisch identiek is aan ons tekst
of html
methoden. Net als bij deze methoden kunnen we attributen zowel krijgen als instellen: we nemen een attribuutnaam en -waarde om in te stellen en alleen een attribuutnaam om te krijgen.
Dome.prototype.attr = function (attr, val) if (typeof val! == "undefined") return this.forEach (function (el) el.setAttribute (attr, val);); else return this.mapOne (function (el) return el.getAttribute (attr);); ;
Als het val
heeft een waarde, we zullen de elementen doorlopen en het geselecteerde attribuut met die waarde instellen, gebruikmakend van de elementen setAttribute
methode. Anders gebruiken we mapOne
om dat kenmerk terug te geven via de getAttribute
methode.
We moeten in staat zijn om nieuwe elementen te maken, zoals elke goede bibliotheek. Natuurlijk zou dit niet goed zijn als een methode op een Koepel
Bijvoorbeeld, dus laten we het recht op onze koepel
voorwerp.
var dome = // verkrijg methode hier create: function (tagName, attrs) ;
Zoals u kunt zien, nemen we twee parameters: de naam van het element en een object met kenmerken. De meeste attributen worden toegepast via onze attr
methode, maar twee krijgen een speciale behandeling. We zullen de gebruiken addClass
methode voor de naam van de klasse
eigendom, en de tekst
methode voor de tekst
eigendom. Natuurlijk moeten we het element en de maken Koepel
object eerst. Hier is alles wat in actie is:
create: function (tagName, attrs) var el = new Dome ([document.createElement (tagName)]); if (attrs) if (attrs.className) el.addClass (attrs.className); verwijder attrs.className; if (attrs.text) el.text (attrs.text); verwijder attrs.text; voor (var-sleutel in attrs) if (attrs.hasOwnProperty (key)) el.attr (key, attrs [key]); return el;
Zoals u kunt zien, maken we het element en sturen het rechtstreeks naar een nieuw element Koepel
voorwerp. Vervolgens behandelen we de attributen. Merk op dat we het moeten verwijderen naam van de klasse
en tekst
attributen na het werken met hen. Dit zorgt ervoor dat ze niet als attributen worden toegepast als we de rest van de toetsen inlopen attrs
. Natuurlijk eindigen we met het retourneren van het nieuwe Koepel
voorwerp.
Maar nu we nieuwe elementen maken, willen we ze meteen in de DOM invoegen?
Vervolgens schrijven we toevoegen
en prepend
methoden, nu zijn dit eigenlijk een beetje lastige functies om te schrijven, voornamelijk vanwege de meervoudige use-cases. Dit is wat we willen kunnen doen:
dome1.append (dome2); dome1.prepend (dome2);
De slechtste browser die we behandelen is IE8.
De use-cases zijn als deze: we willen misschien toevoegen of toevoegen
Opmerking: ik gebruik "nieuw" om elementen te betekenen die nog niet in de DOM voorkomen; bestaande elementen zitten al in de DOM.
Laten we nu beginnen:
Dome.prototype.append = function (els) this.forEach (function (parEl, i) els.forEach (function (childEl) );); ;
We verwachten dat els
parameter om een te zijn Koepel
voorwerp. Een complete DOM-bibliotheek zou dit accepteren als een knooppunt of kniplijst, maar dat zullen we niet doen. We moeten elk van onze elementen in een lus leggen, en vervolgens daarbinnen, lus over elk van de elementen die we willen toevoegen.
Als we het toevoegen els
voor meer dan één element, moeten we ze klonen. We willen de knooppunten echter niet klonen wanneer ze de eerste keer worden toegevoegd, alleen opeenvolgende keren. Dus we doen dit:
if (i> 0) childEl = childEl.cloneNode (true);
Dat ik
komt van de buitenste forEach
loop: het is de index van het huidige parent-element. Als we niet toevoegen aan het eerste bovenliggende element, klonen we het knooppunt. Op deze manier gaat het eigenlijke knooppunt in het eerste bovenliggende knooppunt en krijgt elke andere ouder een kopie. Dit werkt goed, omdat de Koepel
object dat is doorgegeven als een argument, heeft alleen de originele (niet-gekloonde) knooppunten. Dus, als we slechts één element aan een enkel element toevoegen, zullen alle betrokken knooppunten deel uitmaken van hun respectieve Koepel
voorwerpen.
Ten slotte voegen we het element daadwerkelijk toe:
parEl.appendChild (childEl);
Dit is dus alles wat we hebben:
Dome.prototype.append = functie (els) return this.forEach (function (parEl, i) els.forEach (function (childEl) if (i> 0) childEl = childEl.cloneNode (true); parEl .appendChild (childEl););); ;
prepend
MethodeWe willen dezelfde gevallen behandelen voor de prepend
methode, dus de methode is redelijk vergelijkbaar:
Dome.prototype.prepend = function (els) return this.forEach (function (parEl, i) for (var j = els.length -1; j> -1; j--) childEl = (i> 0 )? els [j] .cloneNode (true): els [j]; parEl.insert Before (childEl, parEl.firstChild);); ;
Het verschil bij voorverdelen is dat als je een lijst met elementen achter elkaar plaatst voor een ander element, ze in omgekeerde volgorde eindigen. Omdat we dat niet kunnen forEach
achteruit, ik ga door de lus achteruit met a voor
lus. Nogmaals, we zullen het knooppunt klonen als dit niet de eerste ouder is waaraan we zijn toegevoegd.
Voor onze laatste knooppuntmanipulatiemethode willen we in staat zijn knooppunten van de DOM te verwijderen. Eenvoudig, echt:
Dome.prototype.remove = function () return this.forEach (function (el) return el.parentNode.removeChild (el);); ;
Herhaal gewoon door de knooppunten en bel de removeChild
methode op elk element parentNode
. De schoonheid hier (allemaal dank aan de DOM) is dat dit Koepel
object zal nog steeds prima werken; we kunnen elke gewenste methode gebruiken, inclusief toevoegen of opnieuw plaatsen in de DOM. Leuk, eh?
Last, but zeker not least, gaan we een paar functies schrijven voor event handlers.
Zoals u waarschijnlijk weet, gebruikt IE8 de oude IE-gebeurtenissen, dus we zullen dat moeten controleren. We zullen ook de DOM 0-evenementen inzetten, gewoon omdat we dat kunnen.
Bekijk de methode, en dan bespreken we het:
Dome.prototype.on = (function () if (document.addEventListener) retourfunctie (evt, fn) retourneer this.forEach (function (el) el.addEventListener (evt, fn, false);); ; else if (document.attachEvent) return function (evt, fn) return this.forEach (function (el) el.attachEvent ("on" + evt, fn););; else return-functie (evt, fn) return this.forEach (functie (el) el ["on" + evt] = fn;);; ());
Hier hebben we een IIFE en binnenin doen we functiecontrole. Als document.addEventListener
bestaat, we zullen dat gebruiken; anders zullen we controleren document.attachEvent
of terugvallen op DOM 0-evenementen. Merk op hoe we de laatste functie van de IIFE teruggeven: dat is wat uiteindelijk zal worden toegewezen Dome.prototype.on
. Bij het detecteren van functies is het erg handig om de juiste functie als deze toe te wijzen, in plaats van te controleren op de functies telkens wanneer de functie wordt uitgevoerd.
De uit
functie, die eventhandlers onthaakt, is vrijwel identiek:
Dome.prototype.off = (function () if (document.removeEventListener) return-functie (evt, fn) retourneer this.forEach (function (el) el.removeEventListener (evt, fn, false);); ; else if (document.detachEvent) return function (evt, fn) return this.forEach (function (el) el.detachEvent ("on" + evt, fn););; else return-functie (evt, fn) return this.forEach (functie (el) el ["on" + evt] = null;);; ());
Ik hoop dat je onze kleine bibliotheek een kans geeft en misschien zelfs een beetje uitbreidt. Zoals ik eerder al zei, ik heb het op Github staan, samen met de Jasmine-testsuite voor de code die we hierboven hebben geschreven. Voel het gerust, speel rond en stuur een pull-aanvraag.
Laat me dit nogmaals verduidelijken: het doel van deze tutorial is niet om te suggereren dat je altijd je eigen bibliotheken zou moeten schrijven.
Er zijn toegewijde teams van mensen die samenwerken om de grote, gevestigde bibliotheken zo goed mogelijk te maken. Het punt hier was om een klein kijkje te nemen in wat er in een bibliotheek zou kunnen gebeuren; Ik hoop dat je hier een paar tips hebt opgedaan.
Ik raad je echt aan om een paar van je favoriete bibliotheken te verkennen. Je zult merken dat ze niet zo cryptisch zijn als je zou denken, en je zult waarschijnlijk veel leren. Hier zijn een paar goede plaatsen om te beginnen: