Module, o abordare viitoare a bibliotecilor JavaScript

Bibliotecile JavaScript, cum ar fi jQuery, au fost abordările go-to pentru scrierea JavaScript în browser pentru aproape un deceniu. Au fost un succes imens și intervenția necesară pentru ceea ce a fost cândva un browser de teren plin de discrepanțe și probleme de implementare. jQuery glossed perfect peste bug-uri și quirks browser-ul și a făcut o abordare fără brainer pentru a face lucrurile, cum ar fi manipularea evenimentului, manipularea Ajax și DOM.

În acel moment, jQuery a rezolvat toate problemele noastre, am inclus puterea sa atotputernică și am ajuns să lucrăm imediat. A fost, într-un fel, o cutie neagră pe care browserul "avea nevoie" să o funcționeze corect.

Dar web-ul a evoluat, API-urile se imbunatatesc, standardele sunt in curs de implementare, web-ul este o scena foarte rapida si nu sunt sigur ca bibliotecile gigante au un loc in viitor pentru browser. Ea devine un mediu orientat pe module.

Introduceți modulul

Un modul este o bucată de funcționalitate încapsulată care face doar un singur lucru și un singur lucru foarte bine. De exemplu, un modul poate fi responsabil pentru adăugarea de clase la un element, comunicând prin HTTP prin Ajax și așa mai departe - există nenumărate posibilități.

Un modul poate veni în mai multe forme și dimensiuni, dar scopul general al acestora este să fie importat într-un mediu și să lucreze din cutie. În general, fiecare modul ar avea o bază de documentație pentru dezvoltatori și un proces de instalare, precum și mediile pentru care este destinat (cum ar fi browserul, serverul).

Aceste module devin apoi dependente de proiect, iar dependențele devin ușor de gestionat. Zilele căderii într-o imensă bibliotecă încetinesc încet, bibliotecile mari nu oferă atât flexibilitate, cât și putere. Bibliotecile precum jQuery au recunoscut și acest lucru, ceea ce este fantastic - ei au un instrument online care vă permite să descărcați numai lucrurile de care aveți nevoie.

API-urile moderne reprezintă un imens stimulent pentru inspirația modulului, acum că implementările browserului s-au îmbunătățit drastic, putem începe să creăm module mici de utilitate pentru a ne ajuta să facem sarcinile cele mai comune.

Era modulului este aici și este aici să rămână.

Inspirație pentru un prim modul

Un API modern, de care am fost intotdeauna interesat de la inceput, este API classList. Inspirat din biblioteci, cum ar fi jQuery, am acum un mod nativ de a adăuga clase la un element fără o bibliotecă sau funcții de utilitate.

API-ul classList a fost în jur de câțiva ani, dar nu mulți dezvoltatori știu despre asta. Acest lucru ma inspirat să merg și să creez un modul care să utilizeze API-ul classList, iar acele browsere mai puțin norocoase să o susțină, oferă o formă de implementare de rezervă.

Înainte de a ne scufunda în cod, să vedem ce a adus jQuery la scena pentru adăugarea unei clase la un element:

$ (Elem) .addClass ( 'clasa_mea');

Când această manipulare a aterizat nativ, am ajuns la API-ul classList menționat anterior - un obiect DOMTokenList (valori separate prin spațiu) care reprezintă valorile stocate în clasa elementului unui element. API-ul classList ne oferă câteva metode de a interacționa cu acest DOMTokenList, toate foarte "asemănătoare cu jQuery". Iată un exemplu despre modul în care clasa API adaugă o clasă, care utilizează classList.add () metodă:

elem.classList.add ( 'myclass');

Ce putem învăța din asta? O caracteristică de bibliotecă care se transformă într-o limbă este o afacere destul de mare (sau cel puțin o inspiră). Asta este ceea ce este atât de grozav în ceea ce privește platforma web deschisă, cu toții putem înțelege cum progresează lucrurile.

Deci ce urmează? Știm despre module și avem un fel de API classList, dar, din păcate, nu toate browserele îl acceptă încă. S-ar putea însă scrie o rezervă. Sună ca o idee bună pentru un modul care folosește clasaList atunci când sunt acceptate sau automate backbacks dacă nu.

Crearea unui prim modul: Apollo.js

În jur de șase luni în urmă, am construit un modul independent și foarte ușor pentru adăugarea de clase la un Element în JavaScript simplu - am ajuns să-l numesc apollo.js.

Scopul principal al modulului a fost să înceapă utilizarea genialului API classList și să se îndepărteze de necesitatea unei biblioteci pentru a face o sarcină foarte simplă și obișnuită. jQuery nu a fost (și încă nu) utilizează API-ul classList, așa că am crezut că ar fi o modalitate excelentă de a experimenta noua tehnologie.

Vom trece prin modul în care am făcut-o la fel de bine și gândirea din spatele fiecărei bucăți care formează modulul simplu.

Folosind classList

După cum am văzut deja, classList este un API foarte elegant și "jQuery developer-friendly", trecerea la el este ușoară. Un lucru pe care nu-mi place acest lucru este totuși faptul că trebuie să continuăm să ne referim la obiectul ClassList pentru a folosi una dintre metodele sale. M-am străduit să elimin această repetare atunci când am scris apollo, hotărând asupra următorului design API:

apollo.addClass (elem, 'myclass');

Un bun modul de manipulare a clasei ar trebui să conțină hasClass, addClass, removeClass și toggleClass metode. Toate aceste metode vor ieși din spațiul de nume "apollo".

Privind atent la metoda "addClass" de mai sus, puteți vedea că trec în element ca primul argument. Spre deosebire de jQuery, care este un imens Object personalizat în care sunteți obligat, acest modul va accepta un element DOM, cum este alimentat acel element de până la dezvoltator, metode native sau un modul selector. Al doilea argument este o simplă valoare String, orice nume de clasă vă place.

Să mergem prin celelalte metode de manipulare de clasă pe care am vrut să le creez pentru a vedea cum arată ele:

apollo.hasClass (elem, 'myclass'); apollo.addClass (elem, 'myclass'); apollo.removeClass (elem, 'myclass'); apollo.toggleClass (elem, 'myclass');

Deci, de unde începem? În primul rând, avem nevoie de un obiect pentru a adăuga metodele noastre și de închiderea unor funcții pentru a găzdui orice lucrări interne / variabile / metode. Folosind o expresie de funcție invocată imediat (IIFE), am înfășurat un Obiect numit apollo (și unele metode care conțin abstracții classList) pentru a crea definiția modulului nostru.

(functie () var apollo = ; apollo.hasClass = functie (elem, className) retur elem.classList.contains (className); apollo.addClass = functie (elem, className) elem.classList.add (className);; apollo.removeClass = funcția (elem, className) elem.classList.remove (className);; apollo.toggleClass = funcția (elem, className) elem.classList.toggle (className); window.apollo = apollo;) (); apollo.addClass (document.body, 'test');

Acum, avem clasa de lucru, putem să ne gândim la suportul browserului vechi. Scopul pentru apollomodule este de a oferi o implementare API consistentă și independentă pentru manipularea clasei, indiferent de browser. Acesta este locul unde se face simpla detectare a caracteristicilor.

Modul simplu de a testa prezența caracteristicilor pentru classList este următorul:

dacă ('classList' în document.documentElement) // aveți suport

Folosim în operator care evaluează prezența ClassList la Boolean. Următorul pas ar fi furnizarea condiționată de API numai utilizatorilor care sprijină classList:

() () var apollo = ; var areClass, addClass, removeClass, toggleClass; if ('classList' in document.documentElement) addClass = (elem, className) elem.classList.add (className); removeClass = funcția (elem, className) elem.classList.remove (className); toggleClass = funcția (elem, className) elem.classList.toggle (className); apollo.hasClass = hasClass; apollo.addClass = addClass; apollo.removeClass = removeClass; apollo.toggleClass = toggleClass; window.apollo = apollo;) ();

Suportul legacy poate fi realizat în câteva moduri, prin citirea stringului de clasă și prin introducerea tuturor denumirilor, înlocuirea acestora, adăugarea acestora și așa mai departe. jQuery folosește o mulțime de cod pentru acest lucru, folosind bucle lungi și structură complexă, nu vreau să umflă complet acest modul proaspăt și ușor, așa că am stabilit să folosesc o expresie regulată de potrivire și înlocuire pentru a obține exact același efect cu următoarea nici un cod. 

Iată cea mai curată implementare pe care am putut să o vină:

funcția hasClass (elem, className) returnează noua RegExp ('(^ | \\ s)' + className + '(\\ s | $)') test (elem.className);  funcția addClass (elem, className) if (! hasClass (elem, className)) elem.className + = (elem.className? ": + className;  funcția removeClass (elem, className) if (hasClass (elem, className)) elem.className = elem.className.replace (noul RegExp (' ()) funcția toggleClass (elem, className) (hasClass (elem, className); removeClass: addClass) (elem, className); 

Să le integrăm în modul, adăugând altfel pentru browserele care nu sunt compatibile:

(funcția () var apollo = ; var areClass, addClass, removeClass, toggleClass; if ('classList' în document.documentElement) hasClass = function () return class_classList.contains (className) = funcția (elem, className) elem.classList.add (className); removeClass = funcția (elem, className) elem.classList.remove (className); toggleClass = funcția (elem, className) elem. classList.toggle (className);; altceva hasClass = function (elem, className) returneaza noua RegExp ('(^ | \\ s)' + className + '(\\ s | (elem, className) elem.className + = (elem.className? ":") + className;; removeClass = funcția (elem.className); (elem, className) elem.className = elem.className.replace (noul RegExp ('(^ | \\ s) *' + className + '(\\ s | *);; toggleClass = funcția (elem, className) (hasClass (elem, className); removeClass: addClass) (elem, className);; apollo.has Clasa = hasClass; apollo.addClass = addClass; apollo.removeClass = removeClass; apollo.toggleClass = toggleClass; window.apollo = apollo; ) ();

Un serviciu de lucru pentru ceea ce am făcut până acum.

Să lăsăm-o acolo, conceptul a fost livrat. Modulul apollo are mai multe funcții, cum ar fi adăugarea mai multor clase simultan, puteți verifica dacă sunteți interesat aici.

Deci, ce am făcut? A construit o bucată de funcționalitate încapsulată dedicată a face un singur lucru și un lucru bine. Modulul este foarte simplu de citit și de înțeles, iar modificările pot fi ușor făcute și validate alături de testele unității. De asemenea, avem capacitatea de a atrage apollo pentru proiecte în care nu avem nevoie de jQuery și de oferta sa imensă, iar modulul apollo mic va fi suficient.

Gestionarea dependenței: AMD și CommonJS

Conceptul de module nu este nou, le folosim tot timpul. Probabil că știți că JavaScript nu mai este doar despre browser, ci rulează pe servere și chiar pe televizoare.

Ce modele putem adopta la crearea și utilizarea acestor module noi? Și unde le putem folosi? Există două concepte numite "AMD" și "CommonJS", să le explorăm mai jos.

AMD

Modulul de definire asincron (denumit de obicei AMD) este un API JavaScript pentru definirea modulelor care trebuie încărcate asincron, acestea rulează de obicei în browser deoarece încărcarea sincronă implică costuri de performanță, precum și probleme de accesibilitate, depanare și acces la domenii. AMD poate ajuta dezvoltarea, păstrând modulele JavaScript încapsulate în multe fișiere diferite.

AMD utilizează o funcție numită defini, care definește un modul în sine și orice obiecte de export. Folosind AMD, ne putem referi și la orice dependență de a importa alte module. Un exemplu rapid din proiectul AMD GitHub:

define (['alfa'], funcția (alfa) return verb: function () return alpha.verb () + 2;;);

Am putea face ceva de genul asta pentru apollo dacă am folosi o abordare AMD:

define ('apollo'], funcția (alpha) var apollo = ; var hasClass, addClass, removeClass, toggleClass; conține (className); addClass = funcția (elem, className) elem.classList.add (className); removeClass = funcția (elem, className) elem.classList.remove (className); toggleClass = (elem, className) elem.classList.toggle (className);; altceva hasClass = function (elem, className) retur nou RegExp ('(^ | \ s)' + className + () () () () () () () () () () (nume_clasa_clasa) (nume_clasa_clasa_clasa_clasa_clasa_clasa_clasa_clasa_clasa_clasa_clasa_clasa_clasa_clasa_clasa_clasa_clasa_clasa_class_name) ());; toggleClass = funcția (elem, className) (hasClass (elem, className); removeClass: addClass) sName); ;  apollo.hasClass = hasClass; apollo.addClass = addClass; apollo.removeClass = removeClass; apollo.toggleClass = toggleClass; window.apollo = apollo; ); 

CommonJS

Node.js a crescut în ultimii ani, precum și instrumente și modele de gestionare a dependenței. Node.js utilizează ceva numit CommonJS, care utilizează un obiect "export" pentru a defini conținutul unui modul. O implementare cu adevărat de bază a CommonJS ar putea arăta astfel (ideea de a "exporta" ceva ce trebuie folosit în altă parte):

/ / someModule.js exports.someModule = funcție () return "foo"; ;

Codul de mai sus ar sta în dosarul propriu, l-am numit pe acesta someModule.js. Pentru a le importa în altă parte și pentru a le putea folosi, CommonJS specifică faptul că trebuie să folosim o funcție numită "require" pentru a prelua dependențele individuale:

// face ceva cu 'myModule' var myModule = necesită ('someModule');

Dacă ați folosit și Grunt / Gulp, sunteți obișnuiți să vedeți acest model.

Pentru a folosi acest model cu apollo, am face următoarele și facem referire la exporturi Obiect în loc de fereastră (a se vedea ultimul rând exports.apollo = apollo):

(funcția () var apollo = ; var areClass, addClass, removeClass, toggleClass; if ('classList' în document.documentElement) hasClass = function () return class_classList.contains (className) = funcția (elem, className) elem.classList.add (className); removeClass = funcția (elem, className) elem.classList.remove (className); toggleClass = funcția (elem, className) elem. classList.toggle (className);; altceva hasClass = function (elem, className) returneaza noua RegExp ('(^ | \\ s)' + className + '(\\ s | (elem, className) elem.className + = (elem.className? ":") + className;; removeClass = funcția (elem.className); (elem, className) elem.className = elem.className.replace (noul RegExp ('(^ | \\ s) *' + className + '(\\ s | *);; toggleClass = funcția (elem, className) (hasClass (elem, className); removeClass: addClass) (elem, className);; apollo.has Clasa = hasClass; apollo.addClass = addClass; apollo.removeClass = removeClass; apollo.toggleClass = toggleClass; exports.apollo = apollo; ) ();

Definirea modului universal (UMD)

AMD și CommonJS sunt abordări fantastice, dar dacă am fi creat un modul pe care am vrut să-l lucrăm în toate mediile: AMD, CommonJS și browserul?

Inițial, am făcut câteva dacă și altfel trickery pentru a transmite o funcție fiecărui tip de definiție pe baza a ceea ce era disponibil, am răsucit pentru sprijinul AMD sau CommonJS și l-am folosi dacă era acolo. Această idee a fost apoi adaptată și a început o soluție universală, numită "UMD". Pachetul acesta dacă / altceva trickery pentru noi și noi trecem doar într-o singură funcție ca referință la fiecare tip de modul care a fost suportat, iată un exemplu din depozitul proiectului:

(funcția (rădăcină, fabrica) if (typeof define === 'function' && define.amd) // AMD Înregistrează-te ca un modul anonim. Browser globals root.amdWeb = fabrică (root.b); (aceasta, funcția (b) // folosiți b într-un anumit mod // Întoarceți doar o valoare pentru a defini modulul exportului // Acest exemplu returnează un obiect , dar modulul // poate returna o funcție ca valoare exportată. return ;));

Uau! Multe se întâmplă aici. Transmitem o funcție ca al doilea argument pentru blocul IIFE, care sub numele local al variabilei locale fabrică este atribuită dinamic ca AMD sau global la browser. Da, aceasta nu suportă CommonJS. Cu toate acestea, putem adăuga acel suport (eliminând și comentariile de această dată):

(funcția (rădăcină, fabrica) if (typeof define === 'function' && define.amd) define (['b'], .exports = factory; altceva root.amdWeb = factory (root.b); (aceasta, funcția (b) return ;));

Linia magică este aici module.exports = fabrică care atribuie fabrica noastra companiei CommonJS.

Să înfășurăm apollo în această configurație UMD, astfel încât să poată fi folosită în mediile CommonJS, AMD și browser-ul! Vreau să includ scriptul complet de tip apollo, de la cea mai recentă versiune de pe GitHub, astfel încât lucrurile vor părea puțin mai complexe decât ceea ce am tratat mai sus (au fost adăugate unele caracteristici noi, dar nu au fost incluse în exemplele de mai sus):

/ *! apollo.js v1.7.0 | (c) 2014 @toddmotto | https://github.com/toddmotto/apollo * / (funcție (rădăcină, fabrica) if (typeof define === 'function' && define.amd) define (factory); = "obiect") modul.exports = fabrică; altceva root.apollo = factory ();) (aceasta, funcția () 'use strict'; , toggleClass; var pentruEach = funcția (elemente, fn) if (Object.prototype.toString.call (items)! == '[object Array]') var = = 0; i < items.length; i++)  fn(items[i], i);  ; if ('classList' in document.documentElement)  hasClass = function (elem, className)  return elem.classList.contains(className); ; addClass = function (elem, className)  elem.classList.add(className); ; removeClass = function (elem, className)  elem.classList.remove(className); ; toggleClass = function (elem, className)  elem.classList.toggle(className); ;  else  hasClass = function (elem, className)  return new RegExp('(^|\\s)' + className + '(\\s|$)').test(elem.className); ; addClass = function (elem, className)  if (!hasClass(elem, className))  elem.className += (elem.className ?":") + className;  ; removeClass = function (elem, className)  if (hasClass(elem, className))  elem.className = elem.className.replace(new RegExp('(^|\\s)*' + className + '(\\s|$)*', 'g'),");  ; toggleClass = function (elem, className)  (hasClass(elem, className) ? removeClass : addClass)(elem, className); ;  apollo.hasClass = function (elem, className)  return hasClass(elem, className); ; apollo.addClass = function (elem, classes)  forEach(classes, function (className)  addClass(elem, className); ); ; apollo.removeClass = function (elem, classes)  forEach(classes, function (className)  removeClass(elem, className); ); ; apollo.toggleClass = function (elem, classes)  forEach(classes, function (className)  toggleClass(elem, className); ); ; return apollo; ); 

Am creat un modul pentru a lucra în mai multe medii, oferindu-ne o flexibilitate imensă atunci când aducem noi dependențe în lucrarea noastră - ceva ce o bibliotecă JavaScript nu ne poate oferi fără a o rupe în mici piese funcționale pentru a începe cu.

Testarea

În mod tipic, modulele noastre sunt însoțite de teste de unitate, teste de dimensiuni mici, care ușurează alți dezvoltatori să se alăture proiectului dvs. și să prezinte cereri de tragere pentru îmbunătățiri ale caracteristicilor, este mult mai puțin descurajantă decât o imensă bibliotecă, ! Modulele mici sunt adesea actualizate rapid, în timp ce bibliotecile mai mari pot avea nevoie de timp pentru a implementa noi caracteristici și pentru a repara erorile.

Înfășurarea în sus

A fost minunat să creăm propriul modul și să știm că susținem mulți dezvoltatori în multe medii de dezvoltare. Acest lucru face ca dezvoltarea să devină mai durabilă, mai distractivă și înțelegem instrumentele pe care le folosim mult mai bine. Modulele sunt însoțite de documentația pe care o putem obține până la viteză cu destul de repede și se integrează în munca noastră. Dacă un modul nu se potrivește, am putea găsi fie altul, fie să ne scrieți - ceva ce nu am putut face la fel de ușor cu o bibliotecă mare ca o singură dependență, nu vrem să ne legăm într-o singură soluție.

Bonus: Module ES6

O notă frumoasă pentru a termina, nu a fost minunat să vedem cum au influențat limbile native limbile materne, cum ar fi manipularea claselor? Ei bine, cu ES6 (următoarea generație a limbajului JavaScript), am lovit aur! Avem importuri native și exporturi!

Verificați-l, exportați un modul:

/// funcția myModule.js myModule () // module content export myModule;

Și importarea:

import myModule din "myModule";

Puteți citi mai multe despre ES6 și specificațiile modulelor aici.

Cod