Cómo diseñar una aplicación web usando jquery-mobile y knockoutjs

88

Me gustaría crear una aplicación móvil, elaborada con nada más que html / css y JavaScript. Si bien tengo un conocimiento decente sobre cómo crear una aplicación web con JavaScript, pensé que podría echar un vistazo a un marco como jquery-mobile.

Al principio, pensé que jquery-mobile no era más que un marco de widgets que apunta a navegadores móviles. Muy similar a jquery-ui pero para el mundo móvil. Pero noté que jquery-mobile es más que eso. Viene con un montón de arquitectura y te permite crear aplicaciones con una sintaxis declarativa html. Entonces, para la aplicación más fácil de pensar, no necesitaría escribir una sola línea de JavaScript por sí mismo (lo cual es genial, porque a todos nos gusta trabajar menos, ¿no?)

Para respaldar el enfoque de creación de aplicaciones utilizando una sintaxis html declarativa, creo que es una buena idea combinar jquery-mobile con knockoutjs. Knockoutjs es un marco MVVM del lado del cliente que tiene como objetivo llevar los superpoderes MVVM conocidos de WPF / Silverlight al mundo de JavaScript.

Para mí MVVM es un mundo nuevo. Si bien ya he leído mucho sobre él, nunca antes lo había usado.

Así que esta publicación trata sobre cómo diseñar una aplicación usando jquery-mobile y knockoutjs juntos. Mi idea era escribir el enfoque que se me ocurrió después de mirarlo durante varias horas, y tener un yoda jquery-mobile / knockout para comentarlo, mostrándome por qué apesta y por qué no debería programar en la primera. sitio ;-)

El html

jquery-mobile hace un buen trabajo proporcionando un modelo de estructura básica de páginas. Si bien soy consciente de que podría hacer que mis páginas se carguen a través de ajax después, decidí mantenerlas todas en un archivo index.html. En este escenario básico, estamos hablando de dos páginas para que no sea demasiado difícil estar al tanto de las cosas.

<!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>

El JavaScript

Así que pasemos a la parte divertida: ¡JavaScript!

Cuando comencé a pensar en superponer la aplicación, tenía varias cosas en mente (por ejemplo, capacidad de prueba, acoplamiento flojo). Les voy a mostrar cómo decidí dividir mis archivos y comentar cosas como por qué elegí una cosa sobre otra mientras voy ...

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 es el punto de entrada de mi aplicación. Crea el objeto App y proporciona un espacio de nombres para los modelos de vista (próximamente). Escucha el evento mobileinit que proporciona jquery-mobile.

Como puede ver, estoy creando una instancia de algún tipo de servicio ajax (que veremos más adelante) y la guardo en la variable "servicio".

También conecto el evento pagecreate para la página de inicio en el que creo una instancia de viewModel que obtiene la instancia de servicio pasada. Este punto es esencial para mí. Si alguien piensa, esto debería hacerse de manera diferente, ¡comparta sus pensamientos!

El punto es que el modelo de vista debe operar en un servicio (GetTour /, SaveTour, etc.). Pero no quiero que ViewModel sepa más al respecto. Entonces, por ejemplo, en nuestro caso, solo estoy pasando un servicio ajax simulado porque el backend aún no se ha desarrollado.

Otra cosa que debo mencionar es que ViewModel no tiene ningún conocimiento sobre la vista real. Es por eso que llamo a ko.applyBindings (viewModel, this) desde el controlador pagecreate . Quería mantener el modelo de vista separado de la vista real para que sea más fácil probarlo.

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)

Si bien encontrará la mayoría de los ejemplos de modelos de vista de knockoutjs utilizando una sintaxis literal de objeto, estoy usando la sintaxis de función tradicional con objetos auxiliares 'self'. Básicamente, es cuestión de gustos. Pero cuando desea tener una propiedad observable para hacer referencia a otra, no puede escribir el objeto literal de una vez, lo que lo hace menos simétrico. Esa es una de las razones por las que elijo una sintaxis diferente.

La siguiente razón es el servicio que puedo transmitir como parámetro como mencioné antes.

Hay una cosa más con este modelo de vista que no estoy seguro si elegí la forma correcta. Quiero sondear el servicio ajax periódicamente para obtener los resultados del servidor. Entonces, he elegido implementar los métodos startServicePolling / stopServicePolling para hacerlo. La idea es iniciar el sondeo en pageshow y detenerlo cuando el usuario navegue a una página diferente.

Puede ignorar la sintaxis que se utiliza para sondear el servicio. Es magia RxJS. Solo asegúrese de que lo esté sondeando y actualice las propiedades observables con el resultado devuelto, como puede ver en la parte Suscribir (función (estadísticas) {..}) .

App.MockedStatisticsService.js

Ok, solo queda una cosa por mostrarte. Es la implementación real del servicio. No voy a entrar mucho en detalles aquí. Es solo una simulación que devuelve algunos números cuando se llama a getStatistics . Hay otro método, mockStatistics, que utilizo para establecer nuevos valores a través de la consola js del navegador mientras la aplicación se está ejecutando.

(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, escribí mucho más de lo que inicialmente planeé escribir. Me duele el dedo, mis perros me piden que los lleve a pasear y me siento agotado. Estoy seguro de que faltan muchas cosas aquí y que puse un montón de errores tipográficos y gramaticales. Grítame si algo no está claro y actualizaré la publicación más tarde.

Puede que la publicación no parezca una pregunta, ¡pero en realidad lo es! Me gustaría que compartiera sus pensamientos sobre mi enfoque y si cree que es bueno o malo o si me estoy perdiendo cosas.

ACTUALIZAR

Debido a la gran popularidad que ganó esta publicación y porque varias personas me pidieron que lo hiciera, he puesto el código de este ejemplo en github:

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

¡Consíguelo mientras hace calor!

Christoph
fuente
7
No estoy seguro de que haya una pregunta lo suficientemente específica para que la gente la aborde. Me gustan los detalles que tiene aquí, pero parece que se presta a discusión. En pocas palabras: "Buen blog";)
Bernhard Hofmann
Me alegra que te guste. Me preocupaba un poco que escribiera tanto que la gente teme escribir una respuesta corta. Sin embargo, cualquier discusión es bienvenida. Y si stackoverflow no es el lugar adecuado para iniciar una discusión, podríamos cambiar a los grupos de Google: groups.google.com/forum/#!topic/knockoutjs/80_FuHmCm1s
Christoph
Hola Christoph, ¿cómo te funcionó este enfoque?
hkon
En realidad, me mudé al marco AngularJS más impresionante ;-)
Christoph
1
Esto puede ser mejor si mantuvo solo el primer par de párrafos como pregunta y movió el resto a una auto-respuesta.
rjmunro

Respuestas:

30

Nota: A partir de jQuery 1.7, el .live()método está obsoleto. Úselo .on()para adjuntar controladores de eventos. Los usuarios de versiones anteriores de jQuery deben usar .delegate()con preferencia a .live().

Estoy trabajando en lo mismo (knockout + jquery mobile). Estoy tratando de escribir una publicación de blog sobre lo que he aprendido, pero aquí hay algunos consejos mientras tanto. Recuerde que también estoy tratando de aprender knockout / jquery mobile.

Ver modelo y página

Utilice solo un (1) objeto de modelo de vista por página de jQuery Mobile. De lo contrario, puede tener problemas con los eventos de clic que se activan varias veces.

Ver modelo y haga clic en

Utilice únicamente ko.observable-fields para eventos de clic de modelos de vista.

ko.applyBinding once

Si es posible: solo llame a ko.applyBinding una vez por cada página y use ko.observable's en lugar de llamar a ko.applyBinding varias veces.

pagehide y ko.cleanNode

Recuerde limpiar algunos modelos de vista en la ocultación de páginas. ko.cleanNode parece perturbar la representación de jQuery Mobiles, lo que hace que vuelva a representar el html. Si usa ko.cleanNode en una página, debe eliminar los roles de datos e insertar el html de jQuery Mobile renderizado en el código fuente.

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

pagehide y haga clic en

Si se vincula a eventos de clic, recuerde limpiar .ui-btn-active. La forma más sencilla de lograr esto es usando este fragmento de código:

$('[data-role="page"]').live('pagehide', function() {
    $('.ui-btn-active').removeClass('ui-btn-active');
});
finnsson
fuente
Como mi pregunta fue muy poco específica y usted es el que más trabajo puso en una respuesta, haré suya la respuesta aceptada.
Christoph
¿Alguna vez resolviste esto? Me está costando mucho integrar KO y JQM y no hay buenas guías sobre cómo hacerlo (o una demostración de jsFiddle de un extremo a otro).
Kamranicus
1
No, me mudé al marco AngularJS. Encontré que era superior a KO. Y hay un proyecto de adaptador bastante bueno para hacer de AngularJS / jqm los mejores amigos para siempre: github.com/tigbro/jquery-mobile-angular-adapter. Sin embargo, por lo que hice hasta ahora, pareció exagerado usar ese adaptador. Después de todo, es bastante fácil usar el html / css de jqm y convertir los controles en una directiva Angular: jsfiddle.net/zy7Rg/7
Christoph
Puede crear una estructura que he definido aquí . Estoy seguro de que de esta forma tendrás un control total sobre la aplicación.
Muhammad Raheel