Construiți-vă prima Bibliotecă JavaScript

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:

  • Aceasta nu va fi o bibliotecă complet echipată. Avem un set solid de metode pentru a scrie, dar nu este jQuery. Vom face suficient pentru a vă oferi un sentiment bun pentru problemele pe care le veți întâlni când construiți biblioteci.
  • Nu mergem pentru o compatibilitate completă a browserului peste tot. Ceea ce scriem astăzi ar trebui să funcționeze pe Internet Explorer 8+, Firefox 5+, Opera 10+, Chrome și Safari.
  • Nu vom acoperi toate posibilele utilizări ale bibliotecii noastre. De exemplu, ale noastre 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.


Pasul 1: Crearea bibliotecii Bibliotecii

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.


Pasul 2: Obținerea elementelor

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.


Pasul 3: Crearea Dom Instanțe

Iată 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.


Pasul 4: Adăugarea unei puține utilități

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

O scurtă "filosofică" ocolire

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

Înapoi la Codificare

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

Pasul 5: Lucrul cu textul și HTML

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.


Pasul 6: Clasele de hacking

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


Pasul 7: Fixarea unui bug IE

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.


Pasul 8: Ajustarea atributelor

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


Pasul 9: Crearea elementelor

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?


Pasul 10: Adăugarea și prelungirea elementelor

Î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

  • un element nou pentru unul sau mai multe elemente existente.
  • mai multe elemente noi la unul sau mai multe elemente existente.
  • un element existent la unul sau mai multe elemente existente.
  • mai multe elemente existente la unul sau mai multe elemente existente.

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.


Pasul 11: Înlăturarea nodurilor

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?


Pasul 12: Lucrul cu evenimentele

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

Asta e!

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:

  • 10 lucruri pe care le-am învățat din sursa jQuery (de Paul Irish)
  • 11 Mai multe lucruri pe care le-am învățat din sursa jQuery (de asemenea de Paul Irish)
  • Sub capota lui jQuery (de James Padolsey)
  • Backbone.js: Ghidul hackerilor, partea 1, partea 2, partea 3, partea 4
  • Cunoașteți alte defalcări bune ale bibliotecii? Să le vedem în comentarii!
Cod