web-dev-qa-db-fra.com

Comment architecturer une webapp à l'aide de jquery-mobile et knockoutjs

Je voudrais construire une application mobile, brassée à partir de rien d'autre que du HTML/CSS et du JavaScript. Bien que je sache comment créer une application Web avec JavaScript, j'ai pensé que je pourrais jeter un œil à un cadre comme jquery-mobile.

Au début, je pensais que jquery-mobile n'était rien d'autre qu'un framework de widgets qui cible les 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. Donc, pour l'application la plus facile à penser, vous n'auriez pas besoin d'écrire une seule ligne de JavaScript par vous-même (ce qui est cool, car nous aimons tous travailler moins, n'est-ce pas?)

Pour prendre en charge l'approche de création d'applications à l'aide d'une syntaxe HTML déclarative, je pense que c'est une bonne idée 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à beaucoup lu à ce sujet, je ne l'ai jamais utilisé moi-même auparavant.

Cette publication concerne donc l'architecture d'une application utilisant jquery-mobile et knockoutjs ensemble. Mon idée était d'écrire l'approche que j'ai trouvée après l'avoir regardée pendant plusieurs heures, et d'avoir un yoda jquery-mobile/knockout pour le commenter, en me montrant pourquoi ça craint et pourquoi je ne devrais pas faire de programmation dans le premier endroit ;-)

Le html

jquery-mobile fait un bon travail en fournissant un modèle de structure de base des pages. Bien que je sois bien conscient que je pourrais avoir mes pages à charger via ajax par la suite, j'ai juste décidé de les garder toutes dans un fichier index.html. Dans ce scénario de base, nous parlons de deux pages afin qu'il ne soit pas trop difficile de rester au courant des 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 donc à la partie amusante - le JavaScript!

Quand j'ai commencé à penser à superposer l'application, j'avais plusieurs choses à l'esprit (par exemple, la testabilité, le couplage lâche). Je vais vous montrer comment j'ai décidé de diviser mes fichiers et commenter des choses comme pourquoi ai-je 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 noms pour les modèles de vue (bientôt à venir). Il écoute l'événement mobileinit fourni par jquery-mobile.

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

Je connecte également l'événement pagecreate pour la page d'accueil dans laquelle je crée une instance de viewModel qui obtient l'instance de service transmise. Ce point est essentiel pour moi. Si quelqu'un pense, cela devrait être fait différemment, partagez vos réflexions!

Le fait est que le modèle de vue doit fonctionner sur un service (GetTour /, SaveTour, etc.). Mais je ne veux pas que le ViewModel en sache plus. Ainsi, 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 dois mentionner est que le ViewModel n'a aucune connaissance de la vue réelle. C'est pourquoi j'appelle ko.applyBindings (viewModel, this) depuis le gestionnaire pagecreate. Je voulais garder le modèle de vue séparé de la vue réelle afin de le tester plus facilement.

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)

Bien que vous trouverez la plupart des exemples de modèles de vue knockoutjs en utilisant une syntaxe d'objet littérale, j'utilise la syntaxe de fonction traditionnelle avec des objets d'assistance 'self'. Fondamentalement, c'est une question de goût. Mais lorsque vous voulez avoir une propriété observable pour en référencer une autre, vous ne pouvez pas écrire l'objet littéral en une seule fois, ce qui le rend moins symétrique. C'est l'une des raisons pour lesquelles je choisis une syntaxe différente.

La prochaine raison est le service que je peux transmettre comme paramètre comme je l'ai mentionné précédemment.

Il y a encore une chose avec ce modèle de vue que je ne sais pas si j'ai choisi la bonne façon. Je souhaite interroger périodiquement le service ajax pour récupérer les résultats sur le serveur. J'ai donc choisi d'implémenter les méthodes startServicePolling/ stopServicePolling pour ce faire. L'idée est de démarrer l'interrogation sur pageshow et de l'arrêter lorsque l'utilisateur navigue sur une page différente.

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

App.MockedStatisticsService.js

Ok, il ne reste qu'une chose à vous montrer. Il s'agit de l'implémentation réelle du service. Je n'entre pas beaucoup dans les détails ici. C'est juste une maquette qui retourne quelques chiffres quand getStatistics est appelé. Il existe une autre méthode mockStatistics que j'utilise pour définir de nouvelles valeurs via la console js du navigateur 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)

D'accord, j'ai écrit bien plus que ce que j'avais initialement prévu d'écrire. Mon doigt me fait mal, mes chiens me demandent de les promener et je me sens épuisé. Je suis sûr qu'il manque beaucoup de choses ici et que j'ai mis un tas d'erreurs de frappe et de grammaire. Criez-moi si quelque chose n'est pas clair et je mettrai à jour la publication plus tard.

L'affichage peut ne pas sembler être une question, mais en réalité c'est le cas! J'aimerais que vous partagiez vos réflexions sur mon approche et si vous pensez que c'est bon ou mauvais ou si je manque des choses.

[~ # ~] mise à jour [~ # ~]

En raison de la grande popularité de cette publication et du fait que plusieurs personnes me l'ont demandé, j'ai mis le code de cet exemple sur github:

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

Obtenez-le pendant qu'il fait chaud!

88
Christoph

Remarque: À partir de jQuery 1.7, la méthode .live() est déconseillée. Utilisez .on() pour attacher les gestionnaires d'événements. Les utilisateurs des 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 le knockout/jquery mobile.

Modèle d'affichage et page

N'utilisez qu'un (1) objet modèle de vue par page jQuery Mobile. Sinon, vous pouvez rencontrer des problèmes avec les événements de clic qui sont déclenchés plusieurs fois.

Afficher le modèle et cliquer

Utilisez uniquement les champs ko.observable pour les événements de clic des modèles de vue.

ko.applyBinding une fois

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 d'affichage sur la page de masquage. ko.cleanNode semble perturber le rendu de jQuery Mobiles, ce qui provoque le nouveau rendu du code HTML. Si vous utilisez ko.cleanNode sur une page, vous devez supprimer le rôle de données et insérer le HTML jQuery Mobile rendu dans le code source.

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

masquer la page et cliquer

Si vous vous liez à des événements de clic, n'oubliez pas de nettoyer .ui-btn-active. La façon la 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');
});
30
finnsson