Te-ai minunat vreodată la magia lui Mootools? Te-ai întrebat vreodată cum o face Dojo? Erai curios de gimnastica jQuery? În acest tutorial, ne vom strecura în spatele scenei și vom încerca să construim o versiune foarte simplă a bibliotecii preferate.
Folosim bibliotecile JavaScript aproape în fiecare zi. Când începeți doar să aveți ceva de genul jQuery este fantastic, în principal din cauza DOM. În primul rând, DOM-ul poate fi destul de dur pentru a se bate pentru un începător; este o scuză destul de slabă pentru un API. În al doilea rând, nu este chiar consistent în toate browserele.
Am înfășurat elementele într-un obiect deoarece vrem să putem crea metode pentru obiect.
În acest tutorial, vom face o înjurătură (extrem de superficială) pentru a construi una din aceste biblioteci de la zero. Da, va fi distractiv, dar înainte de a vă face prea entuziasmat, permiteți-mi să clarific câteva lucruri:
adăuga
și Prepend
metodele vor funcționa numai dacă le transmiteți o instanță a bibliotecii noastre; acestea nu vor funcționa cu noduri sau DOM nodeliști.Încă un lucru: în timp ce nu vom scrie teste pentru această bibliotecă, am făcut asta când am dezvoltat acest lucru. Puteți obține biblioteca și testele pe Github.
Vom începe cu un cod de înfășurare, care va conține întreaga noastră bibliotecă. Este expresia dvs. de tip tipic imediat invocată (IIFE).
window.dome = (functie () function Dome (els) var dome = get: functie (selector) ;
După cum puteți vedea, sunăm biblioteca noastră Dome, pentru că este în primul rând o bibliotecă DOM. Da, e lame.
Avem câteva lucruri care se întâmplă aici. În primul rând, avem o funcție; în cele din urmă va fi o funcție de constructor pentru instanțele bibliotecii noastre; aceste obiecte vor împacheta elementele selectate sau create.
Apoi, avem pe noi dom
obiect, care este obiectul nostru real al bibliotecii; după cum vedeți, se întoarce la capăt acolo. Are un gol obține
funcție, pe care o vom folosi pentru a selecta elemente din pagină. Deci, să umplem acum.
dome.get
funcția va lua un parametru, dar ar putea fi o serie de lucruri. Dacă este un șir, vom presupune că este un selector CSS; dar putem lua și un singur Nod DOM sau un NodeList.
get: funcție (selector) var els; dacă (selectorul de tip === "string") els = document.querySelectorAll (selector); altfel dacă (selector.length) els = selector; altceva els = [selector]; a reveni noul Dome (els);
Noi folosim document.querySelectorAll
pentru a simplifica găsirea elementelor: desigur, acest lucru nu limitează suportul browserului nostru, dar pentru acest caz, este în regulă. Dacă selector
nu este un șir, vom verifica a lungime
proprietate. Dacă există, vom ști că avem a NodeList
; în caz contrar, avem un singur element și îl vom pune într-o matrice. Asta pentru că avem nevoie de o matrice care să treacă la apelul nostru Dom
în partea de jos; după cum puteți vedea, vom întoarce un nou Dom
obiect. Deci, să ne întoarcem la acel gol Dom
și completați-l.
Dom
InstanțeIată asta Dom
funcţie:
funcția Dome (els) pentru (var i = 0; i < els.length; i++ ) this[i] = els[i]; this.length = els.length;
Chiar îți recomand să te bagi în interiorul câtorva dintre bibliotecile tale preferate.
Acest lucru este foarte simplu: noi iteram doar asupra elementelor pe care le-am selectat și le lipim pe noul obiect cu indicii numerici. Apoi, adăugăm a lungime
proprietate.
Dar care este punctul aici? De ce nu întoarce doar elementele? Am înfășurat elementele într-un obiect deoarece vrem să putem crea metode pentru obiect; acestea sunt metodele care ne vor permite să interacționăm cu aceste elemente. Aceasta este de fapt o versiune fiartă în jos a felului în care face jQuery.
Deci, acum că avem pe noi Dom
obiect fiind returnat, să adăugăm câteva metode la prototipul său. Voi pune aceste metode direct sub Dom
funcţie.
Primele funcții pe care le vom scrie sunt funcții simple de utilitate. De la noi Dom
obiectele ar putea înfășura mai mult de un element DOM, vom avea nevoie să buclem peste fiecare element în aproape orice metodă; astfel încât aceste utilități vor fi la îndemână.
Să începem cu a Hartă
funcţie:
Dome.prototype.map = funcție (callback) var rezultatele = [], i = 0; pentru (i < this.length; i++) results.push(callback.call(this, this[i], i)); return results; ;
Desigur, Hartă
funcția are un singur parametru, o funcție de apel invers. Vom bate peste elementele din matrice, colectând tot ce este returnat din callback-ul în rezultate
matrice. Observați cum sunăm această funcție de apel invers:
callback.call (acest lucru, acest [i], i));
Făcând acest lucru, funcția va fi chemată în contextul nostru Dom
exemplu, și va primi doi parametri: elementul curent și numărul de index.
De asemenea, dorim a pentru fiecare
funcţie. Acest lucru este de fapt foarte simplu:
Dome.prototype.forEach (apel invers) this.map (apel invers); returnați acest lucru; ;
Deoarece singura diferență între Hartă
și pentru fiecare
este asta Hartă
trebuie să ne întoarcem ceva, putem să ne trimitem apelul this.map
și ignorați matricea returnată; în schimb, ne vom întoarce acest
pentru a face biblioteca noastră în lanț. Vom folosi pentru fiecare
destul de puțin. Deci, observați că atunci când ne întoarcem this.forEach
sunați dintr-o funcție, ne întoarcem de fapt acest
. De exemplu, aceste metode returnează același lucru:
Dome.prototype.someMethod1 = funcție (apel invers) this.forEach (apel invers); returnați acest lucru; ; Dome.prototype.someMethod2 = funcția (apel invers) return this.forEach (apel invers); ;
Încă una: mapOne
. Este ușor să vedem ce face această funcție, dar adevărata întrebare este: de ce avem nevoie de ea? Acest lucru necesită un pic de ceea ce ați putea numi "filozofia bibliotecii".
În primul rând, DOM poate fi destul de dur pentru a încurca un începător; este o scuză destul de slabă pentru un API.
Dacă construirea unei biblioteci a fost doar despre scrierea codului, nu ar fi prea dificil un loc de muncă. Dar, pe măsură ce lucram la acest proiect, am constatat că partea mai dificilă a fost aceea de a decide cum ar trebui să funcționeze anumite metode.
Curând, vom construi o text
care returnează textul elementelor selectate. Dacă noi Dom
obiect mai multe nod DOM (dome.get ( "li")
, de exemplu), ce ar trebui să se întoarcă? Dacă faci ceva similar în jQuery ($ ( "Li"). Text ()
), veți obține un singur șir cu textul tuturor elementelor concatenate împreună. Este util acest lucru? Nu cred, dar nu sunt sigur ce ar fi o valoare de returnare mai bună.
Pentru acest proiect, voi returna textul mai multor elemente ca o matrice, cu excepția cazului în care există un singur element în matrice; atunci vom returna șirul de text, nu un matrice cu un singur element. Cred că veți primi cel mai adesea textul unui singur element, deci optimizăm acest caz. Cu toate acestea, dacă primiți textul mai multor elemente, vom returna ceva cu care puteți lucra.
Asa ca mapOne
metoda se va executa pur și simplu Hartă
, și apoi returnați matricea sau elementul unic care se afla în matrice. Dacă încă nu sunteți sigur ce este util, rămâneți în jur: veți vedea!
Dome.prototype.mapOne = funcție (apel invers) var m = this.map (apel invers); retur m.length> 1? m: m [0]; ;
Apoi, să adăugăm asta text
metodă. La fel ca jQuery, putem să-i transmitem un șir și să setăm textul elementului sau să nu folosim parametri pentru a obține textul înapoi.
Dome.prototype.text = funcția (textul) if (textof text! == "undefined") return this.forEach (funcția (el) el.innerText = text;); altfel return this.mapOne (funcția (el) return el.innerText;); ;
Așa cum vă puteți aștepta, trebuie să verificăm o valoare în text
pentru a vedea dacă suntem sau suntem. Rețineți că doar dacă (text)
nu ar funcționa, deoarece un șir gol este o valoare falsă.
Dacă suntem, o să facem pentru fiecare
peste elementele și setul lor innerText
proprietate la text
. Dacă ajungem, vom întoarce elementele " innerText
proprietate. Rețineți utilizarea de către noi mapOne
metoda: dacă lucrăm cu mai multe elemente, aceasta va întoarce o matrice; altfel, va fi doar șirul.
html
metoda va face destul de mult același lucru ca text
, cu excepția faptului că va folosi innerHTML
proprietate, în loc de innerText
.
Dome.prototype.html = funcția (html) if (tipof html! == "undefined") this.forEach (funcția (el) el.innerHTML = html;); returnați acest lucru; altfel return this.mapOne (funcția (el) return el.innerHTML;); ;
Cum am spus: aproape identic.
În continuare, dorim să putem adăuga și elimina clasele; asa ca sa scriem addClass
și removeClass
metode.
Al nostru addClass
metoda va lua fie un șir sau o serie de nume de clase. Pentru a face acest lucru, trebuie să verificăm tipul acelui parametru. Dacă este o matrice, vom trece peste ea și vom crea un șir de nume de clase. În caz contrar, vom adăuga un singur spațiu în partea din față a numelui clasei, astfel încât să nu se împace cu clasele existente pe element. Apoi, doar buclele peste elementele și adăugați clasele noi la numele clasei
proprietate.
Dome.prototype.addClass = funcția (clase) var className = ""; dacă (tip de clase! == "string") pentru (var i = 0; i < classes.length; i++) className += " " + classes[i]; else className = " " + classes; return this.forEach(function (el) el.className += className; ); ;
Destul de simplu, eh?
Acum, cum rămâne cu eliminarea clasei? Pentru a ne menține simplu, vom permite numai eliminarea unei clase la un moment dat.
Dome.prototype.removeClass = funcția (clazz) return this.forEach (funcția (el) var cs = el.className.split (""), i; în timp ce ((i = cs.indexOf (clazz) 1) cs = cs.slice (0, i) .concat (cs.slice (++ i)); el.className = cs.join ("");); ;
Pe fiecare element, vom împărți el.className
într-o matrice. Apoi, folosim o buclă de timp pentru a elimina clasa de ofensator până la cs.indexOf (clazz)
returnează -1. Facem acest lucru pentru a acoperi cazul de margine în care aceleași clase au fost adăugate la un element de mai multe ori: trebuie să ne asigurăm că este într-adevăr dispărut. Odată ce suntem siguri că am eliminat fiecare instanță a clasei, ne alăturăm matricei cu spații și o pornim el.className
.
Cel mai rău browser cu care ne confruntăm este IE8. În micul nostru bibliotecă, există doar un singur bug IE cu care trebuie să ne ocupăm; din fericire, este destul de simplu. IE8 nu acceptă mulțime
metodă Index de
; îl folosim removeClass
, deci hai să o umplem:
dacă (typeof Array.prototype.indexOf! == "funcția") Array.prototype.indexOf = funcție (element) pentru (var i = 0; i < this.length; i++) if (this[i] === item) return i; return -1; ;
Este destul de simplu și nu este o implementare completă (nu suportă al doilea parametru), dar va funcționa pentru scopurile noastre.
Acum, vrem un an attr
funcţie. Acest lucru va fi ușor, pentru că este practic identic cu cel al nostru text
sau html
metode. Ca și aceste metode, vom fi capabili atât de a obține și de a seta atribute: vom lua un nume de atribut și o valoare pentru a seta, și doar un nume de atribut pentru a obține.
Dome.prototype.attr = funcția (attr, val) if (typeof val! == "undefined") returnați acest lucru pentru fiecare (funcția (el) el.setAttribute (attr, val); else return this.mapOne (funcția (el) return el.getAttribute (attr);); ;
În cazul în care Val
are o valoare, vom trece prin elementele și vom seta atributul selectat cu acea valoare, folosind elementul setAttribute
metodă. În caz contrar, vom folosi mapOne
pentru a returna acel atribut prin getAttribute
metodă.
Ar trebui să putem crea elemente noi, cum ar fi orice bibliotecă bună. Desigur, acest lucru nu ar fi bine ca o metodă pe Dom
exemplu, așa că hai să ne punem bine dom
obiect.
var dome = // obține metoda aici create: funcția (tagName, attrs) ;
După cum puteți vedea, vom lua doi parametri: numele elementului și un obiect de atribute. Majoritatea atributelor se aplică prin intermediul nostru attr
dar două vor primi un tratament special. Vom folosi addClass
metoda pentru numele clasei
proprietate, și text
metoda pentru text
proprietate. Desigur, va trebui să creăm elementul și Dom
obiect primul. Iată totul în acțiune:
crează: funcția (tagName, attrs) var el = noul Dome ([document.createElement (tagName)]); dacă (attrs) if (attrs.className) el.addClass (atribut.className); ștergeți attrs.className; dacă (attrs.text) el.text (attrs.text); ștergeți attrs.text; pentru (var cheie în attrs) if (attrs.hasOwnProperty (cheie)) el.attr (cheie, attrs [cheie]); retur;
După cum puteți vedea, vom crea elementul și îl vom trimite într-un element nou Dom
obiect. Apoi, ne ocupăm de atribute. Observați că trebuie să ștergeți numele clasei
și text
după ce a lucrat cu ele. Acest lucru îi împiedică să fie aplicate ca atribute atunci când le rupem peste restul cheilor din attrs
. Desigur, terminăm prin returnarea noului Dom
obiect.
Dar acum, când creăm elemente noi, vom dori să le inserăm în DOM, corect?
În continuare, o să scriem adăuga
și Prepend
metode, acum, acestea sunt de fapt un pic complicat funcții pentru a scrie, în principal din cauza cazurilor de utilizare multiple. Iată ce vrem să facem:
dome1.append (dome2); dome1.prepend (dome2);
Cel mai rău browser cu care ne confruntăm este IE8.
Cazurile de utilizare sunt după cum urmează: am putea dori să adăugăm sau să ne pregătim
Notă: utilizez "nou" pentru a însemna elemente care nu sunt încă în DOM; elementele existente sunt deja în DOM.
Să facem pasul de acum:
Dome.prototype.append = funcția (els) this.forEach (funcția (parEl, i) els.forEach (funcția (childEl) );); ;
Ne așteptăm ELS
parametru pentru a fi a Dom
obiect. O bibliotecă DOM completă ar accepta acest lucru ca nod sau nodelist, dar nu vom face asta. Trebuie să ne întoarcem peste fiecare dintre elementele noastre, iar apoi în interiorul acestuia, să cuprindem fiecare dintre elementele pe care vrem să le adăugăm.
Dacă îl adăugăm ELS
la mai mult de un element, trebuie să le clonăm. Cu toate acestea, nu vrem să clonăm nodurile prima oară când sunt adăugate, doar ulterior. Deci vom face acest lucru:
dacă (i> 0) childEl = childEl.cloneNode (true);
Acea eu
vine de la exterior pentru fiecare
buclă: este indicele elementului părinte curent. Dacă nu adăugăm la primul element părinte, vom clona nodul. În acest fel, nodul real va merge în primul nod părinte, iar fiecare alt părinte va primi o copie. Acest lucru funcționează bine, deoarece Dom
obiect care a fost transmis ca argument va avea doar nodurile originale (neclonate). Deci, dacă adăugăm doar un singur element unui singur element, toate nodurile implicate vor face parte din respectivele elemente Dom
obiecte.
În sfârșit, vom adăuga elementul:
parEl.appendChild (childEl);
Deci, în totalitate, aceasta este ceea ce avem:
() () ((()) El.forEach (funcția (childEl) if (i> 0) childEl = childEl.cloneNode (true); .appendChild (childEl););); ;
Prepend
MetodăVrem să acoperim aceleași cazuri pentru Prepend
metoda, astfel încât metoda este destul de asemănătoare:
Dome.prototype.prepend = functie (els) returneaza aceasta pentru fiecare (functie (parEl, i) pentru (var j = els.length -1; j> -1; j--) childEl = ); els [j] .cloneNode (adevărat): els [j]; parEl.insertBefore (childEl, parEl.firstChild);); ;
Diferitele atunci când prepending este că, dacă ați prependat succesiv o listă de elemente la un alt element, acestea vor ajunge în ordine inversă. Deoarece nu putem pentru fiecare
înapoi, trec prin buclă înapoi cu o pentru
buclă. Din nou, vom clona nodul dacă acesta nu este primul părinte la care suntem conectați.
Pentru ultima metodă de manipulare a nodului, dorim să putem elimina nodurile din DOM. Ușor, într-adevăr:
Dome.prototype.remove = funcția () return this.forEach (funcția (el) return el.parentNode.removeChild (el);); ;
Doar repetați prin noduri și apelați removeChild
pe fiecare element parentNode
. Frumusețea aici (toate mulțumiri DOM) este că acest lucru Dom
obiect va funcționa în continuare bine; putem folosi orice metodă pe care o dorim, inclusiv adăugarea sau prefixarea acesteia în DOM. Bine, eh?
În cele din urmă, dar cu siguranță nu în ultimul rând, vom scrie câteva funcții pentru gestionarea evenimentelor.
După cum probabil știți, IE8 utilizează vechile evenimente IE, așa că va trebui să verificăm asta. De asemenea, vom arunca în evenimentele DOM 0, doar pentru că putem.
Verificați metoda și apoi o vom discuta:
Dome.prototype.on = (functie () if (document.addEventListener) returneaza functia (evt, fn) returneaza aceasta pentru fiecare (functie (el) el.addEventListener (evt, fn, false); altfel dacă (document.attachEvent) return (evt, fn return this.forEach (funcția (el) el.attachEvent (" returneaza (evt, fn) returneaza aceasta pentru fiecare (functie (el) el ["on" + evt] = fn;);; ());
Aici avem un IIFE, iar în interiorul lui facem verificări. Dacă document.addEventListener
există, vom folosi asta; altfel, vom verifica document.attachEvent
sau să revină la evenimentele DOM 0. Observați cum reluăm funcția finală de la IIFE: la asta se va termina atribuirea Dome.prototype.on
. Când faceți detectarea funcțiilor, este foarte util să puteți atribui funcția adecvată ca aceasta, în loc să verificați caracteristicile de fiecare dată când funcția este executată.
de pe
funcția care dezactivează gestionarea evenimentelor este aproape identică:
Dome.prototype.off = (functie () if (document.removeEventListener) returneaza functia (evt, fn) returneaza aceasta pentru fiecare (functie (el) el.removeEventListener (evt, fn, false); altul dacă (document.detachEvent) return () (return) (întoarcere) (întoarcere) returnează (evt, fn) returnează acest lucru pentru orice (funcția (el) el ["on" + evt] = null;);; ();
Sper să dați bibliotecii noastre o încercare și poate chiar să o extindem puțin. Asa cum am spus mai devreme, am luat-o pe Github, impreuna cu testul Jasmine pentru codul scris mai sus. Simțiți-vă liber să o furci, să jucați în jur și să trimiteți o solicitare de tragere.
Permiteți-mi să clarific din nou: punctul din acest tutorial nu este de a sugera că ar trebui să scrieți întotdeauna propriile biblioteci.
Există echipe dedicate de oameni care lucrează împreună pentru a face bibliotecile mari, stabilite cât mai bine posibil. Scopul era să dai o mică privire în ceea ce se putea întâmpla în interiorul unei biblioteci; Sper că ați luat câteva sfaturi aici.
Chiar îți recomand să te bagi în interiorul câtorva dintre bibliotecile tale preferate. Veți găsi că nu sunt atât de capricioși cum ați fi crezut și probabil că veți învăța foarte mult. Iată câteva locuri minunate pentru a începe: