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!