Construirea de aplicații Knockout.js mari, întreținute și testabile

Knockout.js este un cadru popular MVVM open source (MIT), creat de Steve Sandersen. Site-ul său oferă informații și demo-uri uriașe despre cum se construiesc aplicații simple, dar, din păcate, nu o face pentru aplicații mai mari. Să umplem unele dintre aceste lacune!


AMD și Require.js

AMD este un format de modul JavaScript, iar unul dintre cele mai populare (dacă nu cele mai multe) cadre este http://requirejs.org de către https://twitter.com/jrburke. Se compune din două funcții globale numite require () și defini(), deși require.js include, de asemenea, un fișier JavaScript de pornire, cum ar fi main.js.

Există în primul rând două arome de requ.js: o vanilie require.js fișier și unul care include jQuery (necesită-jquery). În mod natural, acesta din urmă este utilizat în principal pe site-urile cu jQuery. După adăugarea unuia dintre aceste fișiere pe pagina dvs., puteți adăuga apoi următorul cod la dvs. main.js fişier:

solicită (["https://twitter.com/jrburkeapp"], funcția (App) App.init ();)

require () funcția este de obicei utilizată în main.js fișier, dar îl puteți utiliza pentru a include direct un modul oriunde. Acceptă două argumente: o listă de dependențe și o funcție de apel invers.

Funcția de apel invers se execută când toate dependențele finalizează încărcarea, iar argumentele transmise funcției de apel invers sunt obiectele necesar în matricea menționată mai sus.

Este important să rețineți că dependențele se încarcă în mod asincron. Nu toate bibliotecile sunt compatibile cu AMD, dar require.js oferă un mecanism de împingere a acelor tipuri de biblioteci astfel încât acestea să poată fi încărcate.

Acest cod necesită un modul numit aplicaţia, care ar putea arata astfel:

define (["jquery", "ko"], functie ($, ko) var App = function () ; ););

defini() scopul funcției este de a defini a modul. Acceptă trei argumente: numele modulului (care este tipic nu sunt incluse), o listă de dependențe și o funcție de apel invers. defini() vă permite să separați o aplicație în mai multe module, fiecare având o funcție specifică. Acest lucru promovează decuplarea și separarea preocupărilor deoarece fiecare modul are un set propriu de responsabilități specifice.

Folosind Knockout.js și Require.js împreună

Knockout este gata pentru AMD și se definește ca un modul anonim. Nu aveți nevoie să-l ștergeți; doar să o includeți în căile voastre. Cele mai multe plugin-uri Knockout pentru AMD sunt listate mai degrabă ca "knockout" decât "ko", dar puteți utiliza fie valoarea:

request.config (paths: ko: "vânzător / knockout-min", poștă: "vendor / postal", subliniază: "vendor / underscore-min" export: "_", amplifică: export: "amplifică", baseUrl: "/ js");

Acest cod merge în partea de sus main.js. căi opțiunea definește o hartă a modulelor comune care se încarcă cu un nume de cheie, spre deosebire de utilizarea întregului nume de fișier.

shim opțiunea folosește o cheie definită în căi și poate avea două chei speciale numite exporturi și dependențele. exporturi cheia definește modul în care se întoarce modulul shimmed și dependențele definește alte module pe care poate depinde modulul shimmed. De exemplu, shi-ul jQuery Validate ar putea să arate după cum urmează:

shim: // ... "jquery-validate": deps: ["jquery"]

Aplicații Single-versus Multi-Page

Este comun să includeți tot JavaScript-ul necesar într-o aplicație cu o singură pagină. Astfel, puteți defini configurația și cerința inițială a unei aplicații de o singură pagină în main.js ca astfel:

request.config (paths: ko: "vânzător / knockout-min", poștă: "vendor / postal", underscore: "vendor / underscore-min" export: "ko", subliniază: export: "_", amplifică: export: "amplifică", baseUrl: "/ js"); solicită (["https://twitter.com/jrburkeapp"], funcția (App) App.init ();)

S-ar putea să aveți nevoie, de asemenea, de pagini separate, care nu numai că au module specifice unei pagini, ci și un set comun de module. James Burke are două depozite care implementează acest tip de comportament.

Restul acestui articol presupune că construiți o aplicație cu mai multe pagini. Vreau să redenumesc main.js la common.js și include necesar require.config în exemplul de mai sus din dosar. Aceasta este doar pentru semantică.

Acum o voi cere common.js în fișierele mele, după cum urmează:

   

require.config funcția va executa, necesitând fișierul principal pentru pagina specifică. pagini / index fișierul principal ar putea să arate după cum urmează:

solicitați (["app", "postal", "ko", "viewModels / indexViewModel"], funcția (app, postal, ko, indexViewModel) window.app = app; window.postal = postal; ko.applyBindings IndexViewModel ()););

Acest Pagina / index modulul este acum responsabil pentru încărcarea tuturor codurilor necesare pentru index.html pagină. Puteți adăuga alte fișiere principale în directorul de pagini care sunt, de asemenea, responsabile pentru încărcarea modulelor lor dependente. Aceasta vă permite să rupeți aplicațiile pe mai multe pagini în bucăți mai mici, evitând în același timp includerea incorectă a script-urilor (de exemplu, includerea JavaScript pentru index.html în about.html pagină).


Exemple de aplicații

Să scriem o aplicație probă utilizând această abordare. Acesta va afișa o listă cu căutări de marcă de bere și permiteți-ne să alegeți preferințele făcând clic pe numele lor. Aici este structura dosarului aplicației:

Să ne uităm mai întâi la index.html's HTML markup:

Pagini

Structura aplicației noastre folosește mai multe "pagini" sau "rețea" într-un pagini director. Aceste pagini separate sunt responsabile pentru inițializarea fiecărei pagini în aplicație.

ViewModels sunt responsabile de configurarea legăturilor Knockout.

ViewModels

ViewModels este locul unde locuiește principala logică a aplicației Knockout.js. De exemplu, IndexViewModel arata ca:

// https://github.com/jcreamer898/NetTutsKnockout/blob/master/lib/js/viewModels/indexViewModel.js definește (["ko", "underscore", "poștal", "modele / bere", "modele / baseViewModel "," shared / bus "], funcția (ko, _, postal, Beer, BaseViewModel, bus) var IndexViewModel = function () this.beers = [; (aceasta, argumente);; _.extend (IndexViewModel.prototype, BaseViewModel.prototype, initialize: function () // ..., filterBeers: function () / * ... * ) / * ... * /, setupSubscriptions: function () / * ... * /, addToFavorites: function () / * ... * /, removeFromFavorites: function (); returnați IndexViewModel;);

IndexViewModel definește câteva dependențe de bază în partea de sus a fișierului și moșteneste BaseViewModel pentru a inițializa membrii săi ca obiecte observabile knockout.js (vom discuta în scurt timp).

Apoi, mai degrabă decât definirea tuturor funcțiilor ViewModel diferite ca membri de instanță, underscore.js extinde() funcția extinde prototip din IndexViewModel tip de date.

Moștenire și un model de bază

Moștenirea este o formă de reutilizare a codului, permițându-vă să reutilizați funcționalitatea între tipuri similare de obiecte în loc să rescrieți acea funcționalitate. Deci, este util să definiți un model de bază pe care alte modele îl pot moșteni. În cazul nostru, modelul nostru de bază este BaseViewModel:

var BaseViewModel = funcție (opțiuni) this.setup (opțiuni); this.initialize.call (această opțiune); ; _.extend (BaseViewModel.prototype, initialize: function () , _setup: functie (optiuni) var prop; optiuni = optiuni ||  pentru (prop in aceasta) if (this.hasOwnProperty (prop) ) if [opțiuni [prop]] this [prop]] = _.isArray (opțiuni [prop])? ko.observableArray (opțiuni [prop] prop] = _.isArray (acest [prop])? ko.observableArray (acest [prop]): ko.observable (acest [prop]);); returnați BaseViewModel;

BaseViewModel tip defineste doua metode prototip. Primul este inițializa (), care ar trebui să fie suprascris în subtipuri. Al doilea este _înființat(), care stabilește obiectul de legare a datelor.

_înființat metoda buclei asupra proprietăților obiectului. Dacă proprietatea este o matrice, aceasta setează proprietatea ca o observableArray. Se face altceva decât o matrice observabil. De asemenea, verifică pentru oricare dintre valorile inițiale ale proprietăților, folosind valorile implicite, dacă este necesar. Aceasta este o mică abstractizare care elimină necesitatea repetării continue observabil și observableArray funcții.

""acest"Problema

Persoanele care folosesc Knockout au tendința de a prefera membrii instanței peste membrii prototip, din cauza problemelor legate de menținerea valorii corecte a acestora acest. acest cuvântul cheie este o caracteristică complicată a JavaScript-ului, dar nu este atât de rău o dată pe deplin grokked.

Din MDN:

"În general, obiectul obligat acest în domeniul de aplicare curent este determinată de modul în care a fost numită funcția curentă, nu poate fi setată prin atribuire în timpul execuției și poate fi diferită de fiecare dată când se numește funcția. "

Deci, domeniul se schimbă în funcție de modul în care se numește o funcție. Acest lucru este clar evidențiat în jQuery:

var $ el = $ ("#mySuperButton"); $ el.on ("click", functie () // aici, aceasta se refera la buton);

Acest cod stabilește o simplă clic manipularea evenimentului pe un element. Callback-ul este o funcție anonimă și nu face nimic până când cineva face clic pe element. Când se întâmplă acest lucru, domeniul de aplicare al acest în interiorul funcției se referă la elementul DOM real. Ținând cont de acest lucru, luați în considerare următorul exemplu:

var someCallbacks = someVariable: "yay am fost click", mySuperButtonClicked: function () console.log (this.someVariable); ; var $ el = $ ("#mySuperButton"); $ el.on ("faceți clic pe", someCallbacks.mySuperButtonClicked);

E o problemă aici. this.someVariable folosit în interior mySuperButtonClicked () se intoarce nedefinit deoarece acest în apelul de apel se referă mai degrabă la elementul DOM decât la someCallbacks obiect.

Există două modalități de a evita această problemă. Primul utilizează o funcție anonimă ca manipulator de evenimente, care la rândul său solicită someCallbacks.mySuperButtonClicked ():

$ el.on ("faceți clic pe", funcția () someCallbacks.mySuperButtonClicked.apply (););

A doua soluție utilizează fie Function.bind () sau _.lega() metode (Function.bind () nu este disponibil în browserele mai vechi). De exemplu:

$ el.on ("clic", _.bind (someCallbacks.mySuperButtonClicked, someCallbacks));

Fie soluția pe care o alegeți va obține același rezultat final: mySuperButtonClicked () execută în contextul someCallbacks.

"acest"în legături și teste de unitate

În ceea ce privește Knockout, acest problema se poate arăta atunci când lucrați cu legături - în special atunci când vă ocupați $ rădăcină și $ părinte. Ryan Niemeyer a scris un plugin de evenimente delegate care elimină cea mai mare parte această problemă. Acesta vă oferă mai multe opțiuni pentru specificarea funcțiilor, dar puteți utiliza funcția date-clic atribut, iar plugin-ul urcă lanțul dvs. de aplicare și solicită funcția corectă acest.

În acest exemplu, $ parent.addToFavorites se leagă de modelul de vizualizare prin clic legare. Din moment ce

  • elementul se află în interior pentru fiecare obligatoriu, acest interior $ parent.addToFavorites se referă la o instanță a unei bere la care a fost făcut clic.

    Pentru a obține în jurul valorii de acest lucru, _.bindAll metoda asigură acest lucru acest își păstrează valoarea. Prin urmare, adăugați următoarele la inițializa () metoda remediază problema:

    _.extend (IndexViewModel.prototype, BaseViewModel.prototype, initialize: function () this.setupSubscriptions (); this.beerListFiltered = ko.computed (this.filterBeers, this); _.bindAll (aceasta, "addToFavorites") ;,);

    _.bindAll () metoda creează, în esență, un membru de instanță numit adauga la favorite() pe IndexViewModel obiect. Acest nou membru conține versiunea prototip a lui adauga la favorite() care este legat de IndexViewModel obiect.

    acest problema este de ce unele funcții, cum ar fi ko.computed (), acceptă un al doilea argument opțional. Vedeți linia cinci pentru un exemplu. acest a trecut ca al doilea argument care garantează acest lucru acest corect se referă la curent IndexViewModel obiect în interiorul filterBeers.

    Cum testăm acest cod? Să aruncăm o privire mai întâi la adauga la favorite() funcţie:

    addToFavorite: funcția (bere) if (! _) orice (this.favorites (), funcția (b) return.id () === beer.id bere ); 

    Dacă vom folosi cadrul de testare mocha și aștepta.js pentru afirmații, testul unității ar arăta astfel:

    ("bere noua" (nume: "abita amber"), aceasta functionare () a astepta (this.viewModel.favorites () ", id: 3)); // nu poate adăuga bere cu un duplicat id this.viewModel.addToFavorites (noua bere (name:" abita amber ", id: 3)); așteptați (acest.viewModel. () lungime) .to.be (1););

    Pentru a vedea setarea completă de testare a unității, verificați depozitul.

    Să încercăm acum filterBeers (). Mai întâi, să ne uităm la codul său:

    filterBeers: funcția () var filter = this.search () .toLowerCase (); dacă (! filter) returnați această bebe ();  else returnați ko.utils.arrayFilter (this.beers (), funcția (item) return ~ item.name () toLowerCase () indexOf (filter);); ,

    Această funcție utilizează funcția căutare() metodă, care este bazată pe valoare a unui text element în DOM. Apoi utilizează ko.utils.arrayFilter utilitate pentru a căuta și găsi meciuri din lista de beri. beerListFiltered este obligat să

      element în marcă, astfel încât lista de beri poate fi filtrată prin simpla introducere în caseta de text.

      filterBeers funcția, fiind o unitate mică de cod, poate fi testată corespunzător pe unitate:

       (nume: "budweiser", id: 1)); this.viewModel.beers.push (noua bere (nume: "amberbock", id: 2));); ("ar trebui să filtreze o listă de beri", funcția () așteptați (_.isFunction (this.viewModel.beerListFiltered)) .to.be.ok (); this.viewModel.search ("bud"); Aceasta este o metoda care poate fi folosita pentru a obtine o multime de functii care sa aiba o durata de viata. ;

      În primul rând, acest test vă asigură că beerListFiltered este de fapt o funcție. Apoi, se face o interogare prin trecerea valorii "bud" la this.viewModel.search (). Acest lucru ar determina modificarea listei de beri, filtrarea fiecărei bere care nu se potrivește cu "bud". Atunci, căutare este setat la un șir gol pentru a se asigura că beerListFiltered returnează lista completă.


      Concluzie

      Knockout.js oferă multe caracteristici excelente. Atunci când construiți aplicații mari, vă ajută să adoptați multe din principiile discutate în acest articol pentru a vă ajuta codul aplicației să rămână ușor de gestionat, verificabil și să fie întreținut. Consultați aplicația completă, care include câteva subiecte suplimentare, cum ar fi mesagerie. Utilizează postal.js ca un bus de mesaje pentru a transporta mesaje în întreaga aplicație. Folosirea mesajelor într-o aplicație JavaScript poate ajuta la decuplarea părților din aplicație prin eliminarea referințelor greșite. Fii sigur și aruncă o privire!

      Cod