mvvm - example - Cómo arquitectura una aplicación web usando jquery-mobile y knockoutjs
knockout js example (1)
Me gustaría construir una aplicación móvil, elaborada a partir de 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 echarle un vistazo a un marco como jquery-mobile.
Al principio, pensé que jquery-mobile no era más que un framework de widgets dirigido 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 le permite crear aplicaciones con una sintaxis html declarativa. Entonces, para la aplicación más fácil de pensar, no necesitaría escribir una sola línea de JavaScript usted mismo (lo cual es genial, porque a todos nos gusta trabajar menos, ¿no?)
Para apoyar el enfoque de crear aplicaciones usando una sintaxis html declarativa, creo que es una buena idea combinar jquery-mobile con knockoutjs. Knockoutjs es un framework MVVM del lado del cliente que tiene como objetivo llevar los superpoderes de MVVM conocidos de WPF / Silverlight al mundo de JavaScript.
Para mí, MVVM es un mundo nuevo. Si bien ya he leído mucho al respecto, 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 tengo algo de jquery-mobile / knockout yoda para comentarlo, mostrándome por qué apesta y por qué no debería hacer programación en el primero. lugar ;-)
El html
jquery-mobile hace un buen trabajo al proporcionar un modelo de estructura básica de páginas. Si bien soy consciente de que podría cargar mis páginas a través de ajax posteriormente, decidí mantener todas ellas en un solo 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 vayamos a la parte divertida: ¡el JavaScript!
Cuando comencé a pensar en aplicar capas a la aplicación, he tenido varias cosas en mente (por ejemplo, capacidad de prueba, acoplamiento flexible). Voy a mostrarte 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 de la aplicación y proporciona un espacio de nombre para los modelos de vista (próximamente). Escucha el evento mobileinit que ofrece jquery-mobile.
Como puede ver, estoy creando una instancia de algún tipo de servicio ajax (que veremos más adelante) y lo guardo en la variable "servicio".
También conecto el evento pagecreate para la página de inicio en la que creo una instancia de viewModel que transfiere la instancia del servicio. Este punto es esencial para mí. Si alguien piensa que esto debería hacerse de manera diferente, ¡por favor comparta sus ideas!
El punto es que el modelo de vista necesita 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 burlado porque el backend aún no se ha desarrollado.
Otra cosa que debería mencionar es que ViewModel tiene cero conocimiento sobre la vista real. Es por eso que estoy llamando a ko.applyBindings (viewModel, this) desde el controlador de creación de páginas . 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 la mayoría de los knockoutjs ven ejemplos de modelos que usan una sintaxis literal de objeto, estoy usando la sintaxis de la función tradicional con los objetos de un "yo" auxiliar. Básicamente, es una cuestión de gusto. Pero cuando desea tener una propiedad observable para hacer referencia a otra, no puede escribir el literal del objeto de una vez, lo que lo hace menos simétrico. Esa es una de las razones por las que estoy eligiendo una sintaxis diferente.
La siguiente razón es el servicio que puedo transmitir como parámetro como mencioné anteriormente.
Hay una cosa más con este modelo de vista que no estoy seguro si elegí el camino correcto. Quiero sondear el servicio AJAX periódicamente para buscar los resultados del servidor. Por lo tanto, he elegido implementar los métodos startServicePolling / stopServicePolling para hacerlo. La idea es comenzar la votación en páginas y detenerla cuando el usuario navega a otra página.
Puede ignorar la sintaxis que se utiliza para sondear el servicio. Es magia RxJS. Solo asegúrate de sondear y actualizar las propiedades observables con el resultado devuelto, como puedes ver en la parte Subscribe (function (statistics) {..}) .
App.MockedStatisticsService.js
Ok, solo queda una cosa por mostrarte. Es la implementación del servicio real. No entraré mucho en detalles aquí. Es solo un simulacro 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 de los navegadores 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 ya que inicialmente planeé escribir. Me duele el dedo, mis perros me piden que los lleve a caminar y me siento agotado. Estoy seguro de que faltan muchas cosas aquí y de que agregué un montón de errores tipográficos y de gramática. Grítame si algo no está claro y actualizaré la publicación más tarde.
¡La publicación podría no parecer una pregunta, pero realmente lo es! Me gustaría que compartas tus ideas sobre mi enfoque y si piensas que es bueno o malo o si me estoy perdiendo cosas.
ACTUALIZAR
Debido a la gran popularidad que obtuvo esta publicación y porque varias personas me lo pidieron, he puesto el código de este ejemplo en github:
https://github.com/cburgdorf/stackoverflow-knockout-example
¡Consígalo mientras hace calor!
Nota: A partir de jQuery 1.7, el método
.live()
está en desuso. Use.on()
para adjuntar manejadores de eventos. Los usuarios de versiones anteriores de jQuery deben usar.delegate()
en preferencia a.live()
.
Estoy trabajando en lo mismo (knockout + jquery mobile). Intento escribir una publicación en el blog sobre lo que he aprendido, pero aquí hay algunos consejos mientras tanto. Recuerda que también estoy tratando de aprender knockout / jquery mobile.
Ver-Modelo y Página
Solo use un (1) objeto de modelo de vista por jQuery Mobile-page. De lo contrario, puede tener problemas con los eventos de clic que se activan varias veces.
Ver-Modelo y hacer clic
Solo use ko.observable-fields para view-models click-events.
ko.applyBinding una vez
Si es posible, solo llame a ko.applyBinding una vez para cada página y use ko.observable en lugar de llamar a ko.applyBinding varias veces.
pagehide y ko.cleanNode
Recuerde limpiar algunos modelos de vista en la página. ko.cleanNode parece perturbar la renderización de jQuery Mobiles, lo que hace que vuelva a procesar el html. Si utiliza ko.cleanNode en una página, debe eliminar data-role e insertar el jQuery Mobile html representado en el código fuente.
$(''#field'').live(''pagehide'', function() {
ko.cleanNode($(''#field'')[0]);
});
página y clic
Si está vinculando a eventos de clic, recuerde limpiar .ui-btn-active. La forma más fácil de lograr esto es usar este fragmento de código:
$(''[data-role="page"]'').live(''pagehide'', function() {
$(''.ui-btn-active'').removeClass(''ui-btn-active'');
});