Crea la tua prima libreria JavaScript

Ti sei mai meravigliato della magia di Mootools? Vi siete mai chiesti come fa Dojo? Sei mai stato curioso della ginnastica di jQuery? In questo tutorial, ci intrufoleremo dietro le quinte e proveremo a creare una versione super-semplice della tua libreria preferita.

Utilizziamo le librerie JavaScript quasi ogni giorno. Quando hai appena iniziato, avere qualcosa come jQuery è fantastico, principalmente a causa del DOM. In primo luogo, il DOM può essere piuttosto difficile da interpretare come un principiante; è una scusa piuttosto scadente per un'API. In secondo luogo, non è nemmeno coerente su tutti i browser.

Stiamo avvolgendo gli elementi in un oggetto perché vogliamo essere in grado di creare metodi per l'oggetto.

In questo tutorial, faremo una pugnalata (decisamente superficiale) alla creazione di una di queste librerie da zero. Sì, sarà divertente, ma prima che tu sia troppo eccitato, permettimi di chiarire alcuni punti:

  • Questa non sarà una libreria completamente completa. Oh, abbiamo un solido set di metodi per scrivere, ma non è jQuery. Faremo abbastanza per darti un buon feeling con il tipo di problemi che incontrerai quando costruisci le librerie.
  • Non stiamo andando per completa compatibilità con i browser su tutta la linea qui. Quello che stiamo scrivendo oggi dovrebbe funzionare su Internet Explorer 8+, Firefox 5+, Opera 10+, Chrome e Safari.
  • Non copriremo ogni possibile uso della nostra biblioteca. Ad esempio, il nostro aggiungere e anteporre i metodi funzioneranno solo se passi loro un'istanza della nostra libreria; non funzioneranno con nodi DOM o nodelist.

Un'altra cosa: mentre non scriveremo i test per questa libreria, l'ho fatto al primo sviluppo. Puoi ottenere la libreria e i test su Github.


Passaggio 1: creazione della libreria Boilerplate

Inizieremo con un codice wrapper che conterrà la nostra intera libreria. È la tipica espressione di funzione immediatamente invocata (IIFE).

window.dome = (function () function Dome (els)  var dome = get: function (selector) ; return dome; ());

Come puoi vedere, chiamiamo la nostra libreria Dome, perché è principalmente una libreria DOM. Sì, è zoppo.

Abbiamo un paio di cose che succedono qui. Innanzitutto, abbiamo una funzione; alla fine sarà una funzione di costruzione per le istanze della nostra libreria; quegli oggetti avvolgeranno i nostri elementi selezionati o creati.

Quindi, abbiamo il nostro cupola oggetto, che è il nostro oggetto libreria reale; come puoi vedere, viene restituito alla fine lì. Ha un vuoto ottenere funzione, che useremo per selezionare elementi dalla pagina. Quindi, inseritelo adesso.


Passaggio 2: ottenere elementi

Il dome.get la funzione avrà un parametro, ma potrebbe essere un numero di cose. Se si tratta di una stringa, assumeremo che si tratti di un selettore CSS; ma possiamo anche prendere un singolo DOM o un NodeList.

get: function (selector) var els; if (typeof selector === "string") els = document.querySelectorAll (selector);  else if (selector.length) els = selector;  else els = [selector];  return new Dome (els); 

Stiamo usando document.querySelectorAll per semplificare la ricerca di elementi: questo naturalmente limita il supporto del nostro browser, ma per questo caso va bene. Se selettore non è una stringa, controlleremo per a lunghezza proprietà. Se esiste, sapremo di avere un NodeList; altrimenti, abbiamo un singolo elemento e lo inseriremo in un array. Questo perché abbiamo bisogno di un array per passare alla nostra chiamata a Cupola in fondo lì; come puoi vedere, stiamo restituendo un nuovo Cupola oggetto. Quindi torniamo a quello vuoto Cupola funzione e compila.


Passaggio 3: creazione Cupola istanze

Ecco quello Cupola funzione:

function Dome (els) for (var i = 0; i < els.length; i++ )  this[i] = els[i];  this.length = els.length; 

Ti consiglio davvero di scavare all'interno di alcune delle tue librerie preferite.

Questo è davvero semplice: semplicemente iteriamo sugli elementi che abbiamo selezionato e li incolliamo sul nuovo oggetto con indici numerici. Quindi, aggiungiamo a lunghezza proprietà.

Ma qual è il punto qui? Perché non restituire gli elementi? Stiamo avvolgendo gli elementi in un oggetto perché vogliamo essere in grado di creare metodi per l'oggetto; questi sono i metodi che ci permetteranno di interagire con questi elementi. Questa è in realtà una versione ridotta del modo in cui lo fa jQuery.

Quindi, ora che abbiamo il nostro Cupola oggetto restituito, aggiungiamo alcuni metodi al suo prototipo. Ho intenzione di mettere quei metodi proprio sotto il Cupola funzione.


Passaggio 4: aggiunta di alcune utilità

Le prime funzioni che andremo a scrivere sono semplici funzioni di utilità. Dal nostro Cupola gli oggetti potrebbero racchiudere più di un elemento DOM, avremo bisogno di ricorrere ad ogni elemento in quasi tutti i metodi; quindi, queste utility saranno a portata di mano.

Iniziamo con a carta geografica funzione:

Dome.prototype.map = function (callback) var results = [], i = 0; per (; i < this.length; i++)  results.push(callback.call(this, this[i], i));  return results; ;

Certo, il carta geografica la funzione accetta un singolo parametro, una funzione di callback. Ripercorreremo gli elementi dell'array, raccogliendo tutto ciò che viene restituito dal callback in risultati array. Nota come chiameremo questa funzione di callback:

callback.call (questo, questo [i], i));

Facendolo in questo modo, la funzione verrà chiamata nel contesto del nostro Cupola istanza e riceverà due parametri: l'elemento corrente e il numero dell'indice.

Vogliamo anche a per ciascuno funzione. Questo è in realtà molto semplice:

Dome.prototype.forEach (callback) this.map (callback); restituiscilo; ;

Poiché l'unica differenza tra carta geografica e per ciascuno è questo carta geografica ha bisogno di restituire qualcosa, possiamo semplicemente passare il nostro callback a this.map e ignora l'array restituito; invece, torneremo Questo per rendere la nostra libreria concatenabile. Useremo per ciascuno un bel po. Quindi, notalo quando torniamo this.forEach chiamata da una funzione, in realtà stiamo tornando Questo. Ad esempio, questi metodi restituiscono effettivamente la stessa cosa:

Dome.prototype.someMethod1 = function (callback) this.forEach (callback); restituiscilo; ; Dome.prototype.someMethod2 = function (callback) return this.forEach (callback); ;

Ancora uno: mapOne. È facile vedere cosa fa questa funzione, ma la vera domanda è: perché ne abbiamo bisogno? Ciò richiede un po 'di ciò che potresti chiamare "filosofia della biblioteca".

Una breve deviazione "filosofica"

In primo luogo, il DOM può essere piuttosto difficile da risolvere per un principiante; è una scusa piuttosto scadente per un'API.

Se la costruzione di una biblioteca riguardasse solo la scrittura del codice, non sarebbe un lavoro troppo difficile. Ma mentre lavoravo a questo progetto, ho scoperto che la parte più difficile era decidere come dovrebbero funzionare determinati metodi.

Presto, costruiremo un testo metodo che restituisce il testo dei nostri elementi selezionati. Se il nostro Cupola oggetto avvolge diversi nodi DOM (dome.get ( "li"), per esempio), che cosa dovrebbe restituire? Se fai qualcosa di simile in jQuery ($ ( "Li"). Text ()), otterrai una singola stringa con il testo di tutti gli elementi concatenati insieme. È utile? Io non la penso così, ma non sono sicuro di quale sarebbe il miglior valore di ritorno.

Per questo progetto, restituirò il testo di più elementi come una matrice, a meno che non ci sia un solo elemento nell'array; quindi restituiremo semplicemente la stringa di testo, non una matrice con un singolo elemento. Penso che molto spesso otterrai il testo di un singolo elemento, quindi ottimizziamo per quel caso. Tuttavia, se ricevi il testo di più elementi, restituiremo qualcosa su cui puoi lavorare.

Torna a Coding

Così la mapOne il metodo verrà semplicemente eseguito carta geografica, e quindi restituire l'array o il singolo elemento presente nell'array. Se non sei ancora sicuro di come sia utile, restaci: vedrai!

Dome.prototype.mapOne = function (callback) var m = this.map (callback); ritorno m.length> 1? m: m [0]; ;

Passaggio 5: utilizzo di testo e HTML

Avanti, aggiungiamolo testo metodo. Proprio come jQuery, possiamo passargli una stringa e impostare il testo dell'elemento, o non usare parametri per recuperare il testo.

Dome.prototype.text = function (text) if (typeof text! == "undefined") return this.forEach (function (el) el.innerText = text;);  else return this.mapOne (function (el) return el.innerText;); ;

Come ci si potrebbe aspettare, è necessario verificare un valore in testo per vedere se stiamo impostando o ottenendo. Notare quello se (testo) non funzionerebbe, perché una stringa vuota è un valore falso.

Se stiamo impostando, faremo a per ciascuno oltre gli elementi e impostare loro innerText proprietà al testo. Se lo stiamo ottenendo, restituiremo gli elementi ' innerText proprietà. Nota il nostro uso del mapOne metodo: se stiamo lavorando con più elementi, questo restituirà un array; altrimenti, sarà solo la stringa.

Il html il metodo farà praticamente la stessa cosa di testo, tranne che userà il innerHTML proprietà, invece di innerText.

Dome.prototype.html = function (html) if (typeof html! == "undefined") this.forEach (function (el) el.innerHTML = html;); restituiscilo;  else return this.mapOne (function (el) return el.innerHTML;); ;

Come ho detto: quasi identico.


Step 6: Hacking Classes

Prossimo passo, vogliamo essere in grado di aggiungere e rimuovere classi; quindi scriviamo il addClass e removeClass metodi.

Nostro addClass il metodo richiede una stringa o un array di nomi di classi. Per farlo funzionare, dobbiamo controllare il tipo di quel parametro. Se si tratta di un array, eseguiremo il looping e creeremo una stringa di nomi di classi. Altrimenti, aggiungeremo solo un singolo spazio nella parte anteriore del nome della classe, in modo da non compromettere le classi esistenti sull'elemento. Quindi, eseguiamo semplicemente il loop sugli elementi e aggiungiamo le nuove classi al nome della classe proprietà.

Dome.prototype.addClass = function (classes) var className = ""; if (typeof classes! == "stringa") per (var i = 0; i < classes.length; i++)  className += " " + classes[i];   else  className = " " + classes;  return this.forEach(function (el)  el.className += className; ); ;

Piuttosto semplice, eh?

Ora, che ne dici di rimuovere le classi? Per semplificare, consentiremo solo la rimozione di una classe alla volta.

Dome.prototype.removeClass = function (clazz) return this.forEach (function (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 ("");); ;

Su ogni elemento, divideremo il el.className in un array. Quindi, usiamo un ciclo while per dividere la classe offendente fino a quando cs.indexOf (Clazz) restituisce -1. Facciamo questo per coprire il caso limite in cui le stesse classi sono state aggiunte a un elemento più di una volta: dobbiamo assicurarci che sia davvero finito. Una volta sicuri di aver tagliato ogni istanza della classe, ci uniamo alla matrice con gli spazi e la impostiamo el.className.


Passaggio 7: correzione di un bug di IE

Il peggior browser che abbiamo a disposizione è IE8. Nella nostra piccola libreria, c'è solo un bug di IE che dobbiamo affrontare; per fortuna, è piuttosto semplice. IE8 non supporta il schieramento metodo indice di; lo usiamo dentro removeClass, quindi mettiamolo in polistirolo:

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

È piuttosto semplice e non è un'implementazione completa (non supporta il secondo parametro), ma funzionerà per i nostri scopi.


Passaggio 8: regolazione degli attributi

Ora, vogliamo un attr funzione. Sarà facile, perché è praticamente identico al nostro testo o html metodi. Come quei metodi, saremo in grado sia di ottenere che di impostare attributi: prenderemo un nome e un valore di attributo da impostare, e solo un nome di attributo per ottenere.

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

Se la val ha un valore, passeremo in rassegna gli elementi e imposterai l'attributo selezionato con quel valore, usando quello dell'elemento setAttribute metodo. Altrimenti, useremo mapOne per restituire quell'attributo tramite il getAttribute metodo.


Passaggio 9: creazione di elementi

Dovremmo essere in grado di creare nuovi elementi, come ogni buona libreria possibile. Naturalmente, questo non sarebbe un buon metodo come a Cupola esempio, quindi mettiamola sul nostro cupola oggetto.

var dome = // get metodo qui create: function (tagName, attrs) ;

Come puoi vedere, prendiamo due parametri: il nome dell'elemento e un oggetto di attributi. La maggior parte degli attributi viene applicata tramite il nostro attr metodo, ma due riceveranno un trattamento speciale. Useremo il addClass metodo per il nome della classe proprietà, e il testo metodo per il testo proprietà. Certo, avremo bisogno di creare l'elemento e il Cupola prima l'oggetto. Ecco tutto ciò che è in azione:

create: function (tagName, attrs) var el = new Dome ([document.createElement (tagName)]); if (attrs) if (attrs.className) el.addClass (attrs.className); elimina attrs.className;  if (attrs.text) el.text (attrs.text); elimina attrs.text;  for (var key in attrs) if (attrs.hasOwnProperty (key)) el.attr (chiave, attrs [chiave]);  return el; 

Come puoi vedere, creiamo l'elemento e lo inviamo direttamente in un nuovo Cupola oggetto. Quindi, ci occupiamo degli attributi. Si noti che dobbiamo eliminare il nome della classe e testo attributi dopo aver lavorato con loro. Ciò impedisce loro di essere applicati come attributi quando eseguiamo il looping sul resto delle chiavi attrs. Certo, finiamo restituendo il nuovo Cupola oggetto.

Ma ora che stiamo creando nuovi elementi, vorremmo inserirli nel DOM, giusto?


Passaggio 10: aggiunta e premento di elementi

Il prossimo, scriveremo aggiungere e anteporre metodi, ora, queste sono in realtà funzioni un po 'complicate da scrivere, principalmente a causa dei molteplici casi d'uso. Ecco cosa vogliamo essere in grado di fare:

dome1.append (dome2); dome1.prepend (dome2);

Il peggior browser che abbiamo a disposizione è IE8.

I casi d'uso sono come questi: potremmo voler aggiungere o anteporre

  • un nuovo elemento per uno o più elementi esistenti.
  • più nuovi elementi a uno o più elementi esistenti.
  • un elemento esistente a uno o più elementi esistenti.
  • più elementi esistenti su uno o più elementi esistenti.

Nota: sto usando "nuovo" per indicare elementi non ancora presenti nel DOM; gli elementi esistenti sono già nel DOM.

Passiamo ora a questo:

Dome.prototype.append = function (els) this.forEach (function (parEl, i) els.forEach (function (childEl) );); ;

Ci aspettiamo questo els parametro da essere a Cupola oggetto. Una libreria DOM completa lo accetterebbe come nodo o nodelist, ma non lo faremo. Dobbiamo eseguire il ciclo su ciascuno dei nostri elementi, quindi all'interno di esso, eseguiamo il ciclo su ciascuno degli elementi che vogliamo aggiungere.

Se stiamo aggiungendo il els a più di un elemento, abbiamo bisogno di clonarli. Tuttavia, non vogliamo clonare i nodi la prima volta che vengono aggiunti, solo le volte successive. Quindi lo faremo:

if (i> 0) childEl = childEl.cloneNode (true); 

Quello io viene dall'esterno per ciascuno loop: è l'indice dell'elemento genitore corrente. Se non stiamo aggiungendo il primo elemento genitore, cloneremo il nodo. In questo modo, il nodo effettivo andrà nel primo nodo genitore e ogni altro genitore riceverà una copia. Funziona bene, perché il Cupola l'oggetto che è stato passato come argomento avrà solo i nodi originali (non clonati). Quindi, se stiamo aggiungendo un singolo elemento a un singolo elemento, tutti i nodi coinvolti saranno parte dei rispettivi Cupola oggetti.

Infine, in realtà aggiungeremo l'elemento:

parEl.appendChild (childEl);

Quindi, nel complesso, questo è quello che abbiamo:

Dome.prototype.append = function (els) return this.forEach (function (parEl, i) els.forEach (function (childEl) if (i> 0) childEl = childEl.cloneNode (true); parEl .appendChild (childEl););); ;

Il anteporre Metodo

Vogliamo coprire gli stessi casi per il anteporre metodo, quindi il metodo è molto simile:

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.insertBefore (childEl, parEl.firstChild);); ;

Il diverso quando prepending è che se anteponi sequenzialmente un elenco di elementi a un altro elemento, finiranno nell'ordine inverso. Dal momento che non possiamo per ciascuno all'indietro, sto attraversando il ciclo all'indietro con a per ciclo continuo. Nuovamente, cloneremo il nodo se questo non è il primo genitore a cui stiamo aggiungendo.


Passaggio 11: rimozione dei nodi

Per il nostro ultimo metodo di manipolazione del nodo, vogliamo essere in grado di rimuovere i nodi dal DOM. Facile, davvero:

Dome.prototype.remove = function () return this.forEach (function (el) return el.parentNode.removeChild (el);); ;

Basta scorrere i nodi e chiamare il removeChild metodo su ogni elemento parentNode. La bellezza qui (tutto grazie al DOM) è questa Cupola l'oggetto funzionerà ancora bene; possiamo usare qualsiasi metodo che vogliamo su di esso, includendolo o reinserendolo nel DOM. Bello, eh?


Step 12: Lavorare con gli eventi

Ultimo, ma certamente non meno importante, scriveremo alcune funzioni per i gestori di eventi.

Come probabilmente saprai, IE8 utilizza i vecchi eventi di IE, quindi dovremo verificarlo. Inoltre, inseriremo gli eventi DOM 0, solo perché possiamo.

Controlla il metodo e poi ne discuteremo:

Dome.prototype.on = (function () if (document.addEventListener) return function (evt, fn) return 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 function (evt, fn) return this.forEach (function (el) el ["on" + evt] = fn;);; ());

Qui, abbiamo un IIFE, e al suo interno stiamo facendo il controllo delle funzionalità. Se document.addEventListener esiste, lo useremo; altrimenti, controlleremo per document.attachEvent o tornare agli eventi DOM 0. Notate come stiamo restituendo la funzione finale dall'IFFE: questo è ciò che finirà per essere assegnato a Dome.prototype.on. Quando si esegue il rilevamento delle funzioni, è molto utile poter assegnare la funzione appropriata in questo modo, invece di controllare le funzionalità ogni volta che si esegue la funzione.

Il via la funzione, che sgancia i gestori di eventi, è praticamente identica:

Dome.prototype.off = (function () if (document.removeEventListener) return function (evt, fn) return 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 function (evt, fn) return this.forEach (function (el) el ["on" + evt] = null;);; ());

Questo è tutto!

Spero che tu possa provare la nostra piccola libreria e forse anche estenderla un po '. Come ho detto prima, ce l'ho su Github, insieme alla suite di test Jasmine per il codice sopra scritto. Sentiti libero di lanciarlo, giocare, e inviare una richiesta di pull.

Permettetemi di chiarire di nuovo: il punto di questo tutorial non è di suggerire che si dovrebbe sempre scrivere le proprie librerie.

Ci sono team dedicati di persone che lavorano insieme per rendere le biblioteche grandi e consolidate il più buone possibile. Il punto qui era di dare una piccola sbirciatina in quello che potrebbe accadere all'interno di una biblioteca; Spero che tu abbia raccolto alcuni suggerimenti qui.

Ti consiglio davvero di scavare all'interno di alcune delle tue librerie preferite. Scoprirai che non sono così criptici come potresti aver pensato, e probabilmente imparerai molto. Ecco alcuni ottimi punti di partenza:

  • 10 cose che ho imparato da jQuery Source (di Paul Irish)
  • 11 Altre cose che ho imparato da jQuery Source (anche da Paul Irish)
  • Sotto jQuery's Bonnet (di James Padolsey)
  • Backbone.js: Hacker's Guide, part 1, part 2, part 3, part 4
  • Conoscere altri problemi di libreria? Vediamoli nei commenti!