Creare applicazioni Knockout.js grandi, manutenibili e verificabili

Knockout.js è un popolare framework MVVM JavaScript open source (MIT), creato da Steve Sandersen. Il suo sito Web offre grandi informazioni e dimostrazioni su come creare semplici applicazioni, ma sfortunatamente non lo fa per le applicazioni più grandi. Completiamo alcune di queste lacune!


AMD e Require.js

AMD è un formato di modulo JavaScript e uno dei framework più popolari (se non il più) è http://requirejs.org da https://twitter.com/jrburke. Consiste di due funzioni globali chiamate richiedere() e definire(), sebbene require.js incorpori anche un file JavaScript iniziale, come main.js.

Ci sono principalmente due versioni di require.js: una vaniglia require.js file e uno che include jQuery (require-jquery). Naturalmente, quest'ultimo è utilizzato prevalentemente nei siti Web abilitati a jQuery. Dopo aver aggiunto uno di questi file alla tua pagina, puoi aggiungere il seguente codice al tuo main.js file:

require (["https://twitter.com/jrburkeapp"], function (App) App.init ();)

Il richiedere() la funzione è tipicamente usata nel main.js file, ma puoi usarlo per includere direttamente un modulo ovunque. Accetta due argomenti: un elenco di dipendenze e una funzione di callback.

La funzione di callback viene eseguita quando tutte le dipendenze finiscono il caricamento e gli argomenti passati alla funzione di callback sono gli oggetti necessario nella matrice di cui sopra.

È importante notare che le dipendenze vengono caricate in modo asincrono. Non tutte le librerie sono compatibili con AMD, ma require.js fornisce un meccanismo per ridurre questi tipi di librerie in modo che possano essere caricate.

Questo codice richiede un modulo chiamato App, che potrebbe apparire come il seguente:

define (["jquery", "ko"], function ($, ko) var App = function () ; App.prototype.init = function () // INIT ALL TEH THINGS; return new App ( ););

Il definire() Lo scopo della funzione è definire a modulo. Accetta tre argomenti: il nome del modulo (che è tipicamente non incluso), un elenco di dipendenze e una funzione di callback. Il definire() funzione consente di separare un'applicazione in molti moduli, ciascuno con una funzione specifica. Ciò promuove il disaccoppiamento e la separazione delle preoccupazioni poiché ogni modulo ha il proprio insieme di responsabilità specifiche.

Utilizzo di Knockout.js e Require.js insieme

Knockout è pronto per AMD e si definisce come un modulo anonimo. Non hai bisogno di spingerlo; includilo nei tuoi percorsi. La maggior parte dei plug-in Knockout predisposti per AMD lo elencano come "knockout" piuttosto che "ko", ma puoi utilizzare entrambi i valori:

require.config (percorsi: ko: "venditore / knockout-min", postale: "fornitore / postale", trattino basso: "fornitore / trattino basso-min", amplificare: "fornitore / amplificatore", spessore: carattere di sottolineatura: exports: "_", amplify: exports: "amplify", baseUrl: "/ js");

Questo codice è in cima a main.js. Il percorsi opzione definisce una mappa di moduli comuni che vengono caricati con un nome chiave anziché utilizzare l'intero nome del file.

Il shim l'opzione utilizza una chiave definita in percorsi e può avere due chiavi speciali chiamate esportazioni e dipendenze. Il esportazioni key definisce cosa restituisce il modulo shimmed e dipendenze definisce altri moduli da cui potrebbe dipendere il modulo shimmed. Ad esempio, lo shim di jQuery Validate potrebbe essere simile al seguente:

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

Single-vs Apps multi-pagina

È comune includere tutto il codice JavaScript necessario in un'applicazione a singola pagina. Pertanto, è possibile definire la configurazione e il requisito iniziale di un'applicazione a pagina singola in main.js così:

require.config (percorsi: ko: "venditore / knockout-min", postale: "fornitore / postale", trattino basso: "fornitore / trattino basso-min", amplificare: "fornitore / amplificatore", shim: ko: exports: "ko", trattino basso: exports: "_", amplifica: exports: "amplify", baseUrl: "/ js"); require (["https://twitter.com/jrburkeapp"], function (App) App.init ();)

Potresti anche aver bisogno di pagine separate che non hanno solo moduli specifici per la pagina, ma condividono un insieme comune di moduli. James Burke ha due repository che implementano questo tipo di comportamento.

Il resto di questo articolo presuppone che tu stia creando un'applicazione multi-pagina. Rinominerò main.js a common.js e includere il necessario require.config nell'esempio sopra nel file. Questo è puramente per la semantica.

Ora ho bisogno common.js nei miei file, in questo modo:

   

Il require.config la funzione verrà eseguita, richiedendo il file principale per la pagina specifica. Il pagine / index il file principale potrebbe essere simile al seguente:

richiede (["app", "postal", "ko", "viewModels / indexViewModel"], funzione (app, postal, ko, IndexViewModel) window.app = app; window.postal = postal; ko.applyBindings (new IndexViewModel ()););

Questo Pagina / index modulo è ora responsabile del caricamento di tutto il codice necessario per il index.html pagina. È possibile aggiungere altri file principali alla directory delle pagine che sono anche responsabili del caricamento dei moduli dipendenti. Ciò ti consente di spezzare le app multi-pagina in parti più piccole, evitando inutili inclusioni di script (ad esempio includendo il JavaScript per index.html nel about.html pagina).


Applicazione di esempio

Scriviamo un'applicazione di esempio usando questo approccio. Visualizzerà un elenco ricercabile di marche di birra e consentirà di scegliere i tuoi preferiti facendo clic sui loro nomi. Ecco la struttura della cartella dell'app:

Diamo prima un'occhiata index.htmlcodice HTML:

pagine

La struttura della nostra applicazione utilizza più "pagine" o "rete" in a pagine directory. Queste pagine separate sono responsabili dell'inizializzazione di ciascuna pagina nell'applicazione.

Il ViewModels sono responsabili per l'impostazione dei binding Knockout.

ViewModels

Il ViewModels la cartella è dove risiede la logica principale dell'applicazione Knockout.js. Ad esempio, il IndexViewModel sembra il seguente:

// https://github.com/jcreamer898/NetTutsKnockout/blob/master/lib/js/viewModels/indexViewModel.js define (["ko", "underscore", "postal", "models / beer", "models / baseViewModel "," shared / bus "], funzione (ko, _, postal, Beer, BaseViewModel, bus) var IndexViewModel = function () this.beers = []; this.search =" "; BaseViewModel.apply (this, arguments);; _.extend (IndexViewModel.prototype, BaseViewModel.prototype, initialize: function () // ..., filterBeers: function () / * ... * /, parse: function (birre ) / * ... * /, setupSubscriptions: function () / * ... * /, addToFavorites: function () / * ... * /, removeFromFavorites: function () / * ... * /); return IndexViewModel;);

Il IndexViewModel definisce alcune dipendenze di base nella parte superiore del file e eredita BaseViewModel per inizializzare i suoi membri come oggetti osservabili di knockout.js (ne parleremo a breve).

Quindi, anziché definire tutte le varie funzioni di ViewModel come membri di istanza, underscore.js estendere() la funzione estende il prototipo del IndexViewModel tipo di dati.

Ereditarietà e un BaseModel

L'ereditarietà è una forma di riutilizzo del codice che consente di riutilizzare la funzionalità tra tipi di oggetti simili anziché riscrivere quella funzionalità. Quindi, è utile definire un modello base che altri modelli o possano ereditare da. Nel nostro caso, il nostro modello di base è BaseViewModel:

var BaseViewModel = function (options) this._setup (options); this.initialize.call (this, options); ; _.extend (BaseViewModel.prototype, initialize: function () , _setup: function (options) var prop; options = options || ; for (prop in this) if (this.hasOwnProperty (prop) ) if (options [prop]) this [prop] = _.isArray (options [prop])? ko.observableArray (options [prop]): ko.observable (options [prop]); else questo [ prop] = _.isArray (questo [prop])? ko.observableArray (questo [prop]): ko.observable (questo [prop]);); restituire BaseViewModel;

Il BaseViewModel type definisce due metodi su di esso prototipo. Il primo è inizializzare(), che dovrebbe essere sovrascritto nei sottotipi. Il secondo è _impostare(), che imposta l'oggetto per l'associazione dei dati.

Il _impostare il metodo scorre sulle proprietà dell'oggetto. Se la proprietà è una matrice, imposta la proprietà come un observableArray. Qualcosa di diverso da un array è fatto osservabile. Verifica anche i valori iniziali di qualsiasi proprietà, utilizzandoli come valori predefiniti, se necessario. Questa è una piccola astrazione che elimina la necessità di ripetere costantemente il osservabile e observableArray funzioni.

Il "Questo"Problema

Le persone che utilizzano Knockout tendono a preferire i membri di istanza rispetto ai membri prototipo a causa dei problemi con il mantenimento del giusto valore di Questo. Il Questo la parola chiave è una caratteristica complicata di JavaScript, ma non è poi così male una volta completato completamente.

Dal MDN:

"In generale, l'oggetto è legato a Questo nello scope corrente è determinato dal modo in cui è stata chiamata la funzione corrente, non può essere impostata dall'assegnazione durante l'esecuzione e può essere diversa ogni volta che viene chiamata la funzione. "

Quindi, l'ambito cambia a seconda di COME viene chiamata una funzione. Questo è chiaramente evidenziato in jQuery:

var $ el = $ ("#mySuperButton"); $ el.on ("click", function () // qui, si riferisce al pulsante);

Questo codice imposta un semplice clic gestore di eventi su un elemento. Il callback è una funzione anonima e non fa nulla finché qualcuno non fa clic sull'elemento. Quando ciò accade, lo scopo di Questo all'interno della funzione si riferisce all'elemento DOM reale. Tenendo presente ciò, considera il seguente esempio:

var someCallbacks = someVariable: "yay I was clickked", mySuperButtonClicked: function () console.log (this.someVariable); ; var $ el = $ ("#mySuperButton"); $ el.on ("click", someCallbacks.mySuperButtonClicked);

C'è un problema qui. Il this.someVariable usato dentro mySuperButtonClicked () ritorna non definito perché Questo nel callback si riferisce all'elemento DOM piuttosto che al someCallbacks oggetto.

Esistono due modi per evitare questo problema. Il primo utilizza una funzione anonima come gestore di eventi, che a sua volta chiama someCallbacks.mySuperButtonClicked ():

$ el.on ("click", function () someCallbacks.mySuperButtonClicked.apply (););

La seconda soluzione utilizza il Function.bind () o _.bind () metodi (Function.bind () non è disponibile nei browser più vecchi). Per esempio:

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

Qualsiasi soluzione tu scelga otterrà lo stesso risultato finale: mySuperButtonClicked () esegue nel contesto di someCallbacks.

"Questo"in Bindings e Unit Test

In termini di Knockout, il Questo il problema può manifestarsi quando si lavora con i binding, in particolare quando si ha a che fare con $ radice e $ genitore. Ryan Niemeyer ha scritto un plugin per gli eventi delegati che elimina principalmente questo problema. Ti offre diverse opzioni per specificare le funzioni, ma puoi usare il Dati-click attributo e il plugin avvia la catena di portata e chiama la funzione con la corretta Questo.

In questo esempio, $ parent.addToFavorites si lega al modello di vista tramite a clic rilegatura. Dal momento che il

  • elemento risiede dentro a per ciascuno vincolante, il Questo dentro $ parent.addToFavorites si riferisce a un'istanza di una birra su cui è stato fatto clic.

    Per aggirare questo, il _.bindAll il metodo lo assicura Questo mantiene il suo valore. Pertanto, aggiungendo quanto segue al inizializzare() il metodo risolve il problema:

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

    Il _.bindAll () il metodo essenzialmente crea un membro di istanza chiamato Aggiungi ai preferiti() sul IndexViewModel oggetto. Questo nuovo membro contiene la versione prototipo di Aggiungi ai preferiti() che è legato al IndexViewModel oggetto.

    Il Questo il problema è perché alcune funzioni, come ad esempio ko.computed (), accetta un secondo argomento opzionale. Vedi la linea cinque per un esempio. Il Questo passato come il secondo argomento lo assicura Questo si riferisce correttamente alla corrente IndexViewModel oggetto dentro filterBeers.

    Come testeremo questo codice? Diamo prima un'occhiata al Aggiungi ai preferiti() funzione:

    addToFavorites: function (beer) if (! _. any (this.favorites (), function (b) return b.id () === beer.id ();)) this.favorites.push ( birra ); 

    Se usiamo il framework di test di moka e expect.js per le asserzioni, il nostro test di unità sarà simile al seguente:

    ("dovrebbe aggiungere nuove birre ai preferiti", function () expect (this.viewModel.favorites (). length) .to.be (0); this.viewModel.addToFavorites (new Beer (name: "abita amber ", id: 3)); // non può aggiungere birra con un id duplicato this.viewModel.addToFavorites (new Beer (name:" abita amber ", id: 3)); expect (this.viewModel. favoriti (). length) .to.be (1););

    Per vedere l'intera configurazione di prova dell'unità, controlla il repository.

    Ora proviamo filterBeers (). Per prima cosa, diamo un'occhiata al suo codice:

    filterBeers: function () var filter = this.search (). toLowerCase (); if (! filter) return this.beers ();  else return ko.utils.arrayFilter (this.beers (), function (item) return ~ item.name (). toLowerCase (). indexOf (filter);); ,

    Questa funzione utilizza il ricerca() metodo, che è un database per il valore di un testo elemento nel DOM. Quindi usa il ko.utils.arrayFilter utility per cercare e trovare le corrispondenze dall'elenco delle birre. Il beerListFiltered è legato al

      elemento nel markup, quindi l'elenco delle birre può essere filtrato semplicemente digitando nella casella di testo.

      Il filterBeers funzione, essendo una così piccola unità di codice, può essere correttamente testata dall'unità:

       beforeEach (function () this.viewModel = new IndexViewModel (); this.viewModel.beers.push (new Beer (name: "budweiser", id: 1)); this.viewModel.beers.push (new Beer (nome: "amberbock", id: 2));); ("dovrebbe filtrare un elenco di birre", function () expect (_.isFunction (this.viewModel.beerListFiltered)) .to.be.ok (); this.viewModel.search ("bud"); expect ( this.viewModel.filterBeers (). length) .to.be (1); this.viewModel.search (""); expect (this.viewModel.filterBeers (). length) .to.be (2);) ;

      Innanzitutto, questo test fa in modo che il beerListFiltered è in effetti una funzione. Quindi viene eseguita una query passando il valore di "bud" a this.viewModel.search (). Questo dovrebbe far cambiare l'elenco delle birre, filtrando ogni birra che non corrisponde a "bud". Poi, ricerca è impostato su una stringa vuota per garantire che beerListFiltered restituisce l'elenco completo.


      Conclusione

      Knockout.js offre molte fantastiche funzionalità. Quando si creano applicazioni di grandi dimensioni, è utile adottare molti dei principi discussi in questo articolo per aiutare il codice dell'app a rimanere gestibile, verificabile e manutenibile. Guarda l'intera applicazione di esempio, che include alcuni argomenti extra come messaggistica. Utilizza postal.js come bus di messaggi per trasportare messaggi in tutta l'applicazione. L'utilizzo della messaggistica in un'applicazione JavaScript può aiutare a separare parti dell'applicazione rimuovendo i riferimenti rigidi tra loro. Assicurati di dare un'occhiata!