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:
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.
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.
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.
Cupola
istanzeEcco 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.
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".
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.
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]; ;
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.
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
.
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.
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.
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?
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
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););); ;
anteporre
MetodoVogliamo 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.
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?
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;);; ());
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: