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

88

Je voudrais créer une application mobile, conçue à partir de html / css et de JavaScript. Bien que j'aie une bonne connaissance de la création d'une application Web avec JavaScript, j'ai pensé que je pourrais peut-être jeter un coup d'œil dans un cadre comme jquery-mobile.

Au début, je pensais que jquery-mobile n'était rien de plus 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 à imaginer, vous n'avez 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 soutenir l'approche de création d'applications utilisant 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 des super pouvoirs MVVM connus de WPF / Silverlight au monde JavaScript.

Pour moi, MVVM est un nouveau monde. Bien que j'en ai déjà beaucoup lu, je ne l'ai jamais utilisé moi-même auparavant.

Cette publication traite donc de la manière d'architecturer une application en 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 la commenter, me montrant pourquoi ça craint et pourquoi je ne devrais pas faire de programmation dans le premier. endroit ;-)

Le html

jquery-mobile fait du bon travail en fournissant un modèle de structure de base des pages. Bien que je sache bien que je pourrais avoir mes pages à charger via ajax par la suite, j'ai simplement décidé de les conserver 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, testabilité, 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 pars ...

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 je 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 du viewModel qui fait passer l'instance de service. Ce point est essentiel pour moi. Si quelqu'un pense que cela devrait être fait différemment, partagez vos impressions!

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 ne fais que passer un service ajax simulé car 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 séparer le modèle de vue de la vue réelle pour faciliter son test.

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 de fonction traditionnelle avec des objets d'assistance «auto». En gros, 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 le littéral de l'objet 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 raison suivante est le service que je peux transmettre en tant que paramètre, comme je l'ai déjà mentionné.

Il y a encore une chose avec ce modèle de vue dont je ne suis pas sûr d'avoir choisi la bonne manière. Je souhaite interroger périodiquement le service ajax pour récupérer les résultats du serveur. Donc, j'ai choisi d'implémenter les méthodes startServicePolling / stopServicePolling pour ce faire. L'idée est de démarrer le sondage sur pageshow, et de l'arrêter lorsque l'utilisateur accède à 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 que je mets à jour les propriétés observables avec le résultat renvoyé comme vous pouvez le voir dans la partie S'abonner (fonction (statistiques) {..}) .

App.MockedStatisticsService.js

Ok, il ne reste qu'une chose à vous montrer. C'est la mise en œuvre réelle du service. Je ne vais pas beaucoup dans les détails ici. C'est juste un simulacre qui renvoie des nombres lorsque getStatistics est appelé. Il existe une autre méthode mockStatistics que j'utilise pour définir de nouvelles valeurs via la console js des navigateurs 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 comme je l'avais initialement prévu. Mon doigt me fait mal, mes chiens me demandent de les promener et je me sens épuisée. Je suis sûr qu'il manque beaucoup de choses ici et que j'ai mis un tas de fautes de frappe et d'erreurs de grammaire. Criez-moi si quelque chose n'est pas clair et je mettrai à jour la publication plus tard.

La publication peut ne pas sembler une question, mais en fait elle l'est! J'aimerais que vous partagiez vos réflexions sur mon approche et si vous pensez que c'est bon ou mauvais ou si je rate des choses.

METTRE À JOUR

En raison de la grande popularité de cette publication 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

Obtenez-le pendant qu'il fait chaud!

Christoph
la source
7
Je ne suis pas sûr qu'il y ait une question suffisamment précise à laquelle les gens doivent répondre. J'aime le détail que vous avez ici, mais il semble se prêter à la discussion. En moins de mots: "Nice blog";)
Bernhard Hofmann
Je suis heureux que vous ayez aimé. J'étais un peu inquiet d'avoir tellement écrit que les gens ont peur d'écrire une réponse courte. Cependant, toute discussion est la bienvenue. Et si stackoverflow n'est pas le bon endroit pour démarrer une discussion, nous pourrions passer aux groupes Google: groups.google.com/forum/#!topic/knockoutjs/80_FuHmCm1s
Christoph
Salut Christoph, comment cette approche a-t-elle fonctionné pour vous?
hkon
En fait, je suis passé au framework AngularJS plus génial ;-)
Christoph
1
Cela peut être mieux si vous ne conservez que les deux premiers paragraphes comme question et déplacez le reste vers une réponse personnelle.
rjmunro

Réponses:

30

Remarque: à partir de jQuery 1.7, la .live()méthode est obsolète. 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. Rappelez-vous que j'essaie également d'apprendre knockout / jquery mobile.

Modèle de vue et page

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

View-Model et cliquez sur

Utilisez uniquement les champs ko.observables pour les événements de clic de 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 de vue sur pagehide. ko.cleanNode semble perturber le rendu de jQuery Mobiles - en le faisant re-rendre le html. Si vous utilisez ko.cleanNode sur une page, vous devez supprimer les rôles 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 liez à des événements de clic, n'oubliez pas de nettoyer .ui-btn-active. Le moyen le plus simple d'y parvenir consiste à utiliser cet extrait de code:

$('[data-role="page"]').live('pagehide', function() {
    $('.ui-btn-active').removeClass('ui-btn-active');
});
Finnson
la source
Comme ma question était très vague et que vous êtes celui qui a mis le plus de travail sur une réponse, je ferai de la vôtre la réponse acceptée.
Christoph
Avez-vous déjà compris cela? Je passe un sacré temps à intégrer KO et JQM et il n'y a pas de bons guides sur la façon de le faire (ou une démonstration de jsFiddle de bout en bout).
kamranicus
1
Non, je suis passé au framework AngularJS. J'ai trouvé que c'était supérieur à KO. Et il existe un très bon projet d'adaptateur pour faire d'AngularJS / jqm les meilleurs amis pour toujours: github.com/tigbro/jquery-mobile-angular-adapter Cependant, pour ce que j'ai fait jusqu'à présent, il semblait exagéré d'utiliser cet adaptateur. Après tout, il est assez facile d'utiliser simplement le html / css de jqm et de transformer les contrôles en une directive angulaire: jsfiddle.net/zy7Rg/7
Christoph
Vous pouvez créer une structure que j'ai définie ici . Je suis sûr que de cette façon, vous aurez un contrôle total sur l'application.
Muhammad Raheel