如何使用jquery-mobile和knockoutjs构建webapp

Chr*_*oph 88 mvvm jquery-mobile knockout.js

我想构建一个移动应用程序,除了html/css和JavaScript之外别无其他.虽然我对如何使用JavaScript构建Web应用程序有很好的了解,但我想我可能会看一下像jquery-mobile这样的框架.

起初,我认为jquery-mobile仅仅是针对移动浏览器的小部件框架.与jquery-ui非常相似,但对于移动世界而言.但我注意到jquery-mobile不止于此.它带有一堆架构,让你用声明性的html语法创建应用程序.因此,对于最容易思考的应用程序,您不需要自己编写一行JavaScript(这很酷,因为我们都喜欢少工作,不是吗?)

为了支持使用声明性html语法创建应用程序的方法,我认为将jquery-mobile与knockoutjs结合起来是一个很好的选择.Knockoutjs是一个客户端MVVM框架,旨在将WPVM/Silverlight中的MVVM超级功能引入JavaScript世界.

对我来说MVVM是一个新世界.虽然我已经阅读了很多关于它的内容,但我以前从未真正使用它.

所以这篇文章是关于如何使用jquery-mobile和knockoutjs一起构建应用程序.我的想法是写下我看了几个小时后想出来的方法,并有一些jquery-mobile/knockout yoda来评论它,告诉我为什么它糟透了,为什么我不应该在第一次编程地方;-)

HTML

jquery-mobile在提供页面的基本结构模型方面做得很好.虽然我很清楚我之后可以通过ajax加载我的页面,但我决定将它们全部保存在一个index.html文件中.在这个基本场景中,我们谈论的是两个页面,因此不应该太难以掌握一切.

<!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>
Run Code Online (Sandbox Code Playgroud)

JavaScript

让我们来看看有趣的部分 - JavaScript!

当我开始考虑分层应用程序时,我已经考虑了几个方面(例如可测试性,松散耦合).我将向你展示我是如何决定拆分我的文件并评论一些事情,比如我为什么选择一件事而不是另一件事...

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();
  });
});
Run Code Online (Sandbox Code Playgroud)

App.js是我的应用程序的入口点.它创建App对象并为视图模型提供命名空间(即将到来).它监听jquery-mobile提供的mobileinit事件.

正如您所看到的,我正在创建某种ajax服务的实例(我们将在稍后介绍)并将其保存到变量"service"中.

我还连接了主页的pagecreate事件,我在其中创建了一个获取传入服务实例的viewModel实例.这一点对我来说至关重要.如果有人认为,这应该采用不同的方式,请分享您的想法!

关键是,视图模型需要在服务上运行(GetTour /,SaveTour等).但我不希望ViewModel更多地了解它.例如,在我们的例子中,我只是传递一个模拟的ajax服务,因为后端尚未开发.

我应该提到的另一件事是ViewModel对实际视图没有任何了解.这就是我在pagecreate处理程序中调用ko.applyBindings(viewModel,this)的原因.我想保持视图模型与实际视图分离,以便更容易测试它.

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)
Run Code Online (Sandbox Code Playgroud)

虽然你会发现大多数knockoutjs使用对象文字语法查看模型示例,但我使用传统的函数语法和'self'辅助对象.基本上,这是一个品味问题.但是当你想要一个可观察的属性来引用另一个时,你不能一次性写下对象文字,这使得它不那么对称.这就是我选择不同语法的原因之一.

下一个原因是我可以作为参数传递的服务,如前所述.

这个视图模型还有一件事,我不确定我是否选择了正确的方法.我想定期轮询ajax服务以从服务器获取结果.所以,我选择实现startServicePolling/stopServicePolling方法来实现.我们的想法是在pageshow上开始轮询,并在用户导航到不同页面时停止它.

您可以忽略用于轮询服务的语法.这是RxJS的魔力.请确保我正在轮询它并使用返回的结果更新可观察属性,如您在Subscribe(function(statistics){..})部分中所见.

App.MockedStatisticsService.js

好的,还有一件事要告诉你.这是实际的服务实现.我在这里不太详细.它只是一个模拟,在调用getStatistics时会返回一些数字.还有另一种方法mockStatistics,我用它在应用程序运行时通过浏览器js控制台设置新值.

(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)
Run Code Online (Sandbox Code Playgroud)

好吧,我写的更多,就像我最初计划写的那样.我的手指受伤,我的狗要我带他们散步,我感到筋疲力尽.我确信这里缺少很多东西,而且我输入了一堆拼写错误和语法错误.如果有什么不清楚的话,对我大喊大叫,我会在稍后更新帖子.

张贴可能不是一个问题,但实际上它是!我希望你能分享一下你对我的方法的想法,如果你认为它好或坏,或者我错过了什么.

UPDATE

由于这篇文章的主要受欢迎程度,并且有几个人要求我这样做,我已将此示例的代码放在github上:

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

在炎热的时候得到它!

fin*_*son 30

注意:从jQuery 1.7开始,该.live()方法已被弃用.使用.on()附加的事件处理程序.旧版jQuery的用户应该.delegate()优先使用.live().

我正在做同样的事情(淘汰赛+ jquery手机).我正在写一篇关于我所学到的内容的博客文章,但在此期间有一些指示.请记住,我也在尝试学习淘汰赛/ jquery手机.

View-Model和Page

每个jQuery Mobile页面只使用一(1)个视图模型对象.否则,您可能会遇到多次触发的点击事件的问题.

查看模型并单击

仅对视图模型点击事件使用ko.observable-fields.

ko.applyBinding一次

如果可能:只为每个页面调用一次ko.applyBinding并使用ko.observable而不是多次调用ko.applyBinding.

pagehide和ko.cleanNode

请记住在页面隐藏处清理一些视图模型.ko.cleanNode似乎打扰了jQuery Mobiles渲染 - 导致它重新渲染html.如果在页面上使用ko.cleanNode,则需要删除data-role's并在源代码中插入呈现的jQuery Mobile html.

$('#field').live('pagehide', function() {
    ko.cleanNode($('#field')[0]);
});
Run Code Online (Sandbox Code Playgroud)

页面隐藏并单击

如果您绑定点击事件 - 请记得清理.ui-btn-active.完成此操作的最简单方法是使用以下代码段:

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