Como arquitetar um webapp usando jquery-mobile e knockoutjs


88

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!


7
Não tenho certeza se há uma questão suficientemente específica para as pessoas abordarem. Eu gosto do detalhe que você tem aqui, mas parece que pode ser levado à discussão. Em menos palavras: "Belo blog";)
Bernhard Hofmann

Estou feliz que você gostou. Fiquei um pouco preocupado por ter escrito tanto que as pessoas temem escrever uma resposta curta. No entanto, qualquer discussão é bem-vinda. E se stackoverflow for o lugar errado para iniciar uma discussão, poderíamos mudar para grupos do Google: groups.google.com/forum/#!topic/knockoutjs/80_FuHmCm1s
Christoph

Olá, Christoph, como essa abordagem funcionou para você?
hkon

Na verdade, mudei para o framework AngularJS mais incrível ;-)
Christoph

1
Isso pode ser melhor se você mantiver apenas os primeiros parágrafos como a pergunta e mover o resto para uma auto-resposta.
rjmunro

Respostas:


30

Nota: a partir do jQuery 1.7, o .live()método está obsoleto. Use .on()para anexar manipuladores de eventos. Os usuários de versões mais antigas do jQuery devem usar .delegate()preferencialmente .live().

Estou trabalhando na mesma coisa (knockout + jquery mobile). Estou tentando escrever um post sobre o que aprendi, mas aqui estão algumas dicas. Lembre-se de que também estou tentando aprender o knockout / jquery mobile.

Exibir modelo e página

Use apenas um (1) objeto de modelo de visualização por página do jQuery Mobile. Caso contrário, você pode ter problemas com eventos de clique que são disparados várias vezes.

View-Model e clique em

Use ko.observable-fields apenas para eventos de clique de modelos de visualização.

ko.applyBinding uma vez

Se possível: chame ko.applyBinding apenas uma vez para cada página e use ko.observable em vez de chamar ko.applyBinding várias vezes.

pagehide e ko.cleanNode

Lembre-se de limpar alguns modelos de visualização no pagehide. ko.cleanNode parece atrapalhar a renderização do jQuery Mobiles - fazendo com que ele re-renderize o html. Se você usar ko.cleanNode em uma página, será necessário remover data-role's e inserir o jQuery Mobile html renderizado no código-fonte.

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

ocultar a página e clicar

Se você estiver vinculando a eventos de clique - lembre-se de limpar .ui-btn-active. A maneira mais fácil de fazer isso é usando este snippet de código:

$('[data-role="page"]').live('pagehide', function() {
    $('.ui-btn-active').removeClass('ui-btn-active');
});

Como minha pergunta era muito inespecífica e você é quem mais se esforçou para responder, farei a sua a resposta aceita.
Christoph

Você já descobriu isso? Estou tendo problemas para integrar KO e JQM e não há bons guias sobre como fazer isso (ou um jsFiddle demonstrando de ponta a ponta).
Kamranicus

1
Não, mudei para a estrutura AngularJS. Descobri que isso é superior ao KO. E há um projeto de adaptador muito bom para fazer de AngularJS / jqm os melhores amigos para sempre: github.com/tigbro/jquery-mobile-angular-adapter No entanto, pelo que fiz até agora, parecia ser um exagero usar esse adaptador. Afinal, é muito fácil apenas usar o html / css do jqm e transformar os controles em uma diretiva Angular: jsfiddle.net/zy7Rg/7
Christoph

Você pode criar uma estrutura que defini aqui . Tenho certeza que assim você terá controle total sobre o aplicativo.
Muhammad Raheel
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.