Bouw je eerste JavaScript-bibliotheek

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:

  • Dit is geen volledig complete bibliotheek. Oh, we hebben een solide set van methoden om te schrijven, maar het is geen jQuery. We zullen genoeg doen om u een goed gevoel te geven voor het soort problemen waar u tegenaan loopt bij het bouwen van bibliotheken.
  • We gaan hier niet voor volledige browsercompatibiliteit over de hele linie. Wat we vandaag schrijven, zou moeten werken op Internet Explorer 8+, Firefox 5+, Opera 10+, Chrome en Safari.
  • We gaan niet elk mogelijk gebruik van onze bibliotheek behandelen. Bijvoorbeeld onze 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.


Stap 1: De Library Boilerplate maken

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.


Stap 2: Elementen ophalen

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.


Stap 3: Maken Koepel instanties

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


Stap 4: Een paar hulpprogramma's toevoegen

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

Een korte "filosofische" omweg

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.

Terug naar codering

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

Stap 5: Werken met tekst en HTML

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.


Stap 6: klassen hacken

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.


Stap 7: Een IE-bug repareren

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.


Stap 8: Attributen aanpassen

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.


Stap 9: Elementen maken

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?


Stap 10: Elementen toevoegen en voorbereiden

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

  • een nieuw element voor een of meer bestaande elementen.
  • meerdere nieuwe elementen voor een of meer bestaand element.
  • een bestaand element voor een of meer bestaande elementen.
  • meerdere bestaande elementen voor een of meer bestaande elementen.

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

De prepend Methode

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


Stap 11: Knopen verwijderen

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?


Stap 12: Werken met gebeurtenissen

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

Dat is het!

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:

  • 10 dingen die ik heb geleerd van de jQuery-bron (door Paul Irish)
  • 11 Meer dingen die ik heb geleerd van de jQuery-bron (ook door Paul Irish)
  • Under jQuery's Bonnet (door James Padolsey)
  • Backbone.js: Hacker's Guide, deel 1, deel 2, deel 3, deel 4
  • Ken je andere goede bibliotheekuitbraken? Laten we ze in de reacties zien!