88 votes

Comment architecturer une application web en utilisant jquery-mobile et knockoutjs

Je voudrais construire une application mobile, élaborée à partir de rien d'autre que du html/css et du JavaScript. Bien que j'aie une bonne connaissance de la façon de construire une application web avec JavaScript, j'ai pensé que je pourrais jeter un coup d'œil à un framework comme jquery-mobile.

Au début, je pensais que jquery-mobile n'était rien d'autre qu'un cadre de widgets destiné aux navigateurs mobiles. Très similaire à jquery-ui mais pour le monde mobile. Mais j'ai remarqué que jquery-mobile est plus que cela. Il est livré avec un tas d'architecture et vous permet de créer des applications avec une syntaxe html déclarative. Ainsi, pour l'application la plus facile à concevoir, vous n'aurez pas besoin d'écrire une seule ligne de JavaScript par vous-même (ce qui est cool, parce que nous aimons tous travailler moins, n'est-ce pas ?)

Pour soutenir l'approche consistant à créer des applications à l'aide d'une syntaxe html déclarative, je pense qu'il est judicieux de combiner jquery-mobile avec knockoutjs. Knockoutjs est un framework MVVM côté client qui vise à apporter les super pouvoirs MVVM connus de WPF/Silverlight au monde JavaScript.

Pour moi, MVVM est un nouveau monde. Bien que j'aie déjà lu beaucoup de choses à son sujet, je ne l'ai encore jamais utilisé moi-même.

Ce message porte sur l'architecture d'une application utilisant conjointement jquery-mobile et knockoutjs. Mon idée était d'écrire l'approche que j'ai trouvée après l'avoir examinée pendant plusieurs heures, et de demander à un yoda de jquery-mobile/knockout de la commenter, en me montrant pourquoi elle est nulle et pourquoi je ne devrais pas programmer en premier lieu ;-)

Le html

jquery-mobile fait un bon travail en fournissant un modèle de structure de base des pages. Bien que je sois conscient que je pourrais faire en sorte que mes pages soient chargées par ajax par la suite, j'ai simplement décidé de les garder toutes dans un seul fichier index.html. Dans ce scénario de base, nous parlons de deux pages, de sorte qu'il ne devrait pas être trop difficile de garder le dessus sur les choses.

<!DOCTYPE html> 
<html> 
  <head> 
  <title>Page Title</title> 
  <link rel="stylesheet" href="libs/jquery-mobile/jquery.mobile-1.0a4.1.css" />
  <link rel="stylesheet" href="app/base/css/base.css" />
  <script src="libs/jquery/jquery-1.5.0.min.js"></script>
  <script src="libs/knockout/knockout-1.2.0.js"></script>
  <script src="libs/knockout/knockout-bindings-jqm.js" type="text/javascript"></script>
  <script src="libs/rx/rx.js" type="text/javascript"></script>
  <script src="app/App.js"></script>
  <script src="app/App.ViewModels.HomeScreenViewModel.js"></script>
  <script src="app/App.MockedStatisticsService.js"></script>
  <script src="libs/jquery-mobile/jquery.mobile-1.0a4.1.js"></script>  
</head> 
<body> 

<!-- Start of first page -->
<div data-role="page" id="home">

    <div data-role="header">
        <h1>Demo App</h1>
    </div><!-- /header -->

    <div data-role="content">   

    <div class="ui-grid-a">
        <div class="ui-block-a">
            <div class="ui-bar" style="height:120px">
                <h1>Tours today (please wait 10 seconds to see the effect)</h1>
                <p><span data-bind="text: toursTotal"></span> total</p>
                <p><span data-bind="text: toursRunning"></span> running</p>
                <p><span data-bind="text: toursCompleted"></span> completed</p>     
            </div>
        </div>
    </div>

    <fieldset class="ui-grid-a">
        <div class="ui-block-a"><button data-bind="click: showTourList, jqmButtonEnabled: toursAvailable" data-theme="a">Tour List</button></div>  
    </fieldset>

    </div><!-- /content -->

    <div data-role="footer" data-position="fixed">
        <h4>by Christoph Burgdorf</h4>
    </div><!-- /header -->
</div><!-- /page -->

<!-- tourlist page -->
<div data-role="page" id="tourlist">

    <div data-role="header">
        <h1>Bar</h1>
    </div><!-- /header -->

    <div data-role="content">   
        <p><a href="#home">Back to home</a></p> 
    </div><!-- /content -->

    <div data-role="footer" data-position="fixed">
        <h4>by Christoph Burgdorf</h4>
    </div><!-- /header -->
</div><!-- /page -->

</body>
</html>

Le JavaScript

Venons-en à la partie amusante : le JavaScript !

Lorsque j'ai commencé à réfléchir à la stratification de l'application, j'avais plusieurs choses en tête (par exemple, la testabilité, le couplage souple). Je vais vous montrer comment j'ai décidé de diviser mes fichiers et commenter des choses comme pourquoi j'ai choisi une chose plutôt qu'une autre pendant que je vais...

App.js

var App = window.App = {};
App.ViewModels = {};

$(document).bind('mobileinit', function(){
    // while app is running use App.Service.mockStatistic({ToursCompleted: 45}); to fake backend data from the console
    var service = App.Service = new App.MockedStatisticService();    

  $('#home').live('pagecreate', function(event, ui){
        var viewModel = new App.ViewModels.HomeScreenViewModel(service);
        ko.applyBindings(viewModel, this);
        viewModel.startServicePolling();
  });
});

App.js est le point d'entrée de mon application. Il crée l'objet App et fournit un espace de nom pour les modèles de vue (bientôt disponibles). Il écoute l'appel mobileinit que jquery-mobile fournit.

Comme vous pouvez le voir, je crée une instance d'une sorte de service ajax (que nous verrons plus tard) et je la sauvegarde dans la variable "service".

J'ai aussi branché le pagecreate pour la page d'accueil, dans lequel je crée une instance du viewModel qui reçoit l'instance de service transmise. Ce point est essentiel pour moi. Si quelqu'un pense que cela devrait être fait différemment, merci de partager vos idées !

Le fait est que le modèle de vue doit opérer sur un service (GetTour/, SaveTour etc.). Mais je ne veux pas que le ViewModel en sache plus à ce sujet. Donc, par exemple, dans notre cas, je passe juste un service ajax simulé parce que le backend n'a pas encore été développé.

Une autre chose que je devrais mentionner est que le ViewModel n'a aucune connaissance de la vue actuelle. C'est la raison pour laquelle j'appelle ko.applyBindings(viewModel, this) à partir de l'application pagecreate manipulateur. Je voulais que le modèle de vue soit séparé de la vue elle-même pour faciliter les tests.

App.ViewModels.HomeScreenViewModel.js

(function(App){
  App.ViewModels.HomeScreenViewModel = function(service){
    var self = {}, disposableServicePoller = Rx.Disposable.Empty;

    self.toursTotal = ko.observable(0);
    self.toursRunning = ko.observable(0);
    self.toursCompleted = ko.observable(0);
    self.toursAvailable = ko.dependentObservable(function(){ return this.toursTotal() > 0; }, self);
    self.showTourList = function(){ $.mobile.changePage('#tourlist', 'pop', false, true); };        
    self.startServicePolling = function(){  
        disposableServicePoller = Rx.Observable
            .Interval(10000)
            .Select(service.getStatistics)
            .Switch()
            .Subscribe(function(statistics){
                self.toursTotal(statistics.ToursTotal);
                self.toursRunning(statistics.ToursRunning); 
                self.toursCompleted(statistics.ToursCompleted); 
            });
    };
    self.stopServicePolling = disposableServicePoller.Dispose;      

    return self; 
  };
})(App)

Alors que vous trouverez la plupart des exemples de modèles de vue knockoutjs utilisant une syntaxe littérale d'objet, j'utilise la syntaxe traditionnelle de fonction avec un objet d'aide. En fait, c'est une question de goût. Mais quand vous voulez qu'une propriété observable fasse référence à une autre, vous ne pouvez pas écrire le littéral d'objet en une seule fois, ce qui le rend moins symétrique. C'est une des raisons pour lesquelles j'ai choisi une syntaxe différente.

La raison suivante est le service que je peux transmettre en tant que paramètre, comme je l'ai déjà mentionné.

Il y a une autre chose avec ce modèle de vue que je ne suis pas sûr d'avoir choisi de la bonne façon. Je veux interroger le service ajax périodiquement pour récupérer les résultats du serveur. Donc, j'ai choisi d'implémenter startServicePolling / stopServicePolling méthodes pour y parvenir. L'idée est de lancer le sondage lors de la présentation de la page, et de l'arrêter lorsque l'utilisateur navigue vers une autre page.

Vous pouvez ignorer la syntaxe qui est utilisée pour interroger le service. C'est la magie de RxJS. Assurez-vous simplement que j'interroge le service et que je mets à jour les propriétés de l'observable avec le résultat renvoyé, comme vous pouvez le voir dans l'exemple suivant Subscribe(function(statistics){..}) partie.

App.MockedStatisticsService.js

Ok, il ne reste qu'une chose à vous montrer. C'est l'implémentation réelle du service. Je ne vais pas entrer dans les détails ici. C'est juste un simulateur qui renvoie des nombres quand getStatistics est appelé. Il existe une autre méthode mockStatistics que j'utilise pour définir de nouvelles valeurs via la console du navigateur js pendant que l'application est en cours d'exécution.

(function(App){
    App.MockedStatisticService = function(){
        var self = {},
        defaultStatistic = {
            ToursTotal: 505,
            ToursRunning: 110,
            ToursCompleted: 115 
        },
        currentStatistic = $.extend({}, defaultStatistic);;

        self.mockStatistic = function(statistics){
            currentStatistic = $.extend({}, defaultStatistic, statistics);
        };

        self.getStatistics = function(){        
            var asyncSubject = new Rx.AsyncSubject();
            asyncSubject.OnNext(currentStatistic);
            asyncSubject.OnCompleted();
            return asyncSubject.AsObservable();
        };

        return self;
    };
})(App)

Ok, j'ai écrit beaucoup plus que ce que j'avais initialement prévu d'écrire. J'ai mal au doigt, mes chiens me demandent de les emmener en promenade et je suis épuisée. Je suis sûre qu'il manque plein de choses ici et que j'ai mis un tas de fautes de frappe et de grammaire. Criez-moi si quelque chose n'est pas clair et je mettrai à jour l'article plus tard.

L'affichage peut ne pas sembler être une question, mais en fait, c'en est une ! J'aimerais que vous me fassiez part de vos réflexions sur mon approche et que vous me disiez si vous pensez qu'elle est bonne ou mauvaise ou si je passe à côté de certaines choses.

UPDATE

En raison de la grande popularité de ce message et parce que plusieurs personnes m'ont demandé de le faire, j'ai mis le code de cet exemple sur github :

https://github.com/cburgdorf/stackoverflow-knockout-example

Prenez-le pendant qu'il est chaud !

30voto

finnsson Points 1937

Nota: À partir de la version 1.7 de jQuery, la fonction .live() est dépréciée. Utilisez .on() pour attacher des gestionnaires d'événements. Les utilisateurs d'anciennes versions de jQuery doivent utiliser .delegate() de préférence à .live() .

Je travaille sur la même chose (knockout + jquery mobile). J'essaie d'écrire un article de blog sur ce que j'ai appris, mais voici quelques conseils en attendant. N'oubliez pas que j'essaie également d'apprendre knockout/jquery mobile.

Modèle de vue et page

N'utilisez qu'un (1) objet view-model par page jQuery Mobile. Sinon, vous risquez d'avoir des problèmes avec les événements de clics qui sont déclenchés plusieurs fois.

View-Model et cliquez

N'utilisez ko.observable-fields que pour les événements de clic des modèles de vue.

ko.applyBinding once

Si possible : n'appelez ko.applyBinding qu'une seule fois pour chaque page et utilisez ko.observable au lieu d'appeler ko.applyBinding plusieurs fois.

pagehide et ko.cleanNode

N'oubliez pas de nettoyer certains modèles de vue sur pagehide. ko.cleanNode semble perturber le rendu de jQuery Mobiles - ce qui entraîne un nouveau rendu du html. Si vous utilisez ko.cleanNode sur une page, vous devez supprimer les data-role's et insérer le html rendu de jQuery Mobile dans le code source.

$('#field').live('pagehide', function() {
    ko.cleanNode($('#field')[0]);
});

pagehide et cliquez

Si vous utilisez des événements de clics, n'oubliez pas de nettoyer .ui-btn-active. Le moyen le plus simple d'y parvenir est d'utiliser cet extrait de code :

$('[data-role="page"]').live('pagehide', function() {
    $('.ui-btn-active').removeClass('ui-btn-active');
});

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X