Eu gostaria de construir um aplicativo móvel, desenvolvido a partir de nada mais que html / css e JavaScript. Embora eu tenha um conhecimento decente de como construir um aplicativo da web com JavaScript, pensei que poderia dar uma olhada em uma estrutura como jquery-mobile.
No início, pensei que o jquery-mobile não era nada mais do que uma estrutura de widget voltada para navegadores móveis. Muito semelhante ao jquery-ui, mas para o mundo móvel. Mas percebi que o jquery-mobile é mais do que isso. Ele vem com um monte de arquitetura e permite que você crie aplicativos com uma sintaxe html declarativa. Portanto, para o aplicativo mais fácil de pensar, você não precisaria escrever uma única linha de JavaScript sozinho (o que é legal, porque todos nós gostamos de trabalhar menos, não é?)
Para dar suporte à abordagem de criação de aplicativos usando uma sintaxe html declarativa, acho que é uma boa ideia combinar jquery-mobile com knockoutjs. Knockoutjs é uma estrutura MVVM do lado do cliente que visa trazer superpoderes MVVM conhecidos do WPF / Silverlight para o mundo do JavaScript.
Para mim, o MVVM é um novo mundo. Embora eu já tenha lido muito sobre isso, na verdade nunca usei antes.
Portanto, esta postagem é sobre como arquitetar um aplicativo usando jquery-mobile e knockoutjs juntos. Minha ideia era escrever a abordagem que eu vim com depois de olhar para ela por várias horas e ter algum jquery-mobile / knockout yoda para comentá-la, me mostrando por que é uma merda e por que eu não deveria fazer programação no primeiro Lugar, colocar ;-)
O html
jquery-mobile faz um bom trabalho fornecendo um modelo de estrutura básica de páginas. Embora eu esteja bem ciente de que poderia carregar minhas páginas via ajax posteriormente, decidi apenas mantê-las em um arquivo index.html. Neste cenário básico, estamos falando de duas páginas para que não seja muito difícil ficar por dentro das coisas.
<!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>
O JavaScript
Então, vamos para a parte divertida - o JavaScript!
Quando comecei a pensar em camadas do aplicativo, tinha várias coisas em mente (por exemplo, testabilidade, acoplamento fraco). Vou mostrar como decidi dividir meus arquivos e comentar coisas como por que escolhi uma coisa em vez de outra enquanto vou ...
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 é o ponto de entrada do meu aplicativo. Ele cria o objeto App e fornece um namespace para os modelos de visualização (em breve). Ele escuta o evento mobileinit que o jquery-mobile fornece.
Como você pode ver, estou criando uma instância de algum tipo de serviço ajax (que veremos mais tarde) e salvando-a na variável "serviço".
Eu também conecto o evento pagecreate para a página inicial na qual crio uma instância do viewModel que obtém a instância do serviço passada. Este ponto é essencial para mim. Se alguém pensa, isso deveria ser feito de forma diferente, por favor, compartilhe sua opinião!
A questão é que o modelo de visualização precisa operar em um serviço (GetTour /, SaveTour etc.). Mas não quero que o ViewModel saiba mais sobre isso. Por exemplo, em nosso caso, estou apenas transmitindo um serviço Ajax simulado porque o back-end ainda não foi desenvolvido.
Outra coisa que devo mencionar é que o ViewModel tem conhecimento zero sobre a visão real. É por isso que estou chamando ko.applyBindings (viewModel, this) de dentro do manipulador pagecreate . Eu queria manter o modelo de visualização separado da visualização real para tornar mais fácil testá-lo.
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)
Embora você encontre a maioria dos exemplos de modelos de visualização de knockoutjs usando uma sintaxe literal de objeto, estou usando a sintaxe de função tradicional com objetos auxiliares 'self'. Basicamente, é uma questão de gosto. Mas quando você deseja que uma propriedade observável faça referência a outra, você não pode escrever o literal do objeto de uma vez, o que o torna menos simétrico. Esse é um dos motivos pelos quais estou escolhendo uma sintaxe diferente.
O próximo motivo é o serviço que posso passar como parâmetro, conforme mencionei antes.
Há mais uma coisa com esse modelo de visualização que não tenho certeza se escolhi o caminho certo. Quero pesquisar o serviço ajax periodicamente para buscar os resultados do servidor. Portanto, optei por implementar os métodos startServicePolling / stopServicePolling para fazer isso. A ideia é iniciar a votação no pagehow e interrompê-la quando o usuário navegar para outra página.
Você pode ignorar a sintaxe que é usada para pesquisar o serviço. É mágica RxJS. Apenas certifique-se de que estou pesquisando e atualizando as propriedades observáveis com o resultado retornado, como você pode ver na parte Assinar (função (estatísticas) {..}) .
App.MockedStatisticsService.js
Ok, só falta mostrar a você. É a implementação real do serviço. Não vou entrar em muitos detalhes aqui. É apenas uma simulação que retorna alguns números quando getStatistics é chamado. Há outro método mockStatistics que uso para definir novos valores por meio do console js do navegador enquanto o aplicativo está em execução.
(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, escrevi muito mais do que planejei inicialmente. Meu dedo dói, meus cachorros me pedem para levá-los para passear e me sinto exausto. Tenho certeza de que há muitas coisas faltando aqui e que coloquei um monte de erros de digitação e de gramática. Grite comigo se algo não estiver claro e eu irei atualizar a postagem mais tarde.
A postagem pode não parecer uma pergunta, mas na verdade é! Gostaria que você compartilhasse seus pensamentos sobre minha abordagem e se você acha que é boa ou ruim ou se estou perdendo coisas.
ATUALIZAR
Devido à grande popularidade que esta postagem ganhou e porque várias pessoas me pediram para fazer isso, coloquei o código deste exemplo no github:
https://github.com/cburgdorf/stackoverflow-knockout-example
Obtenha enquanto está quente!