Mvvm 如何使用jquery mobile和knockoutjs构建Web应用程序

Mvvm 如何使用jquery mobile和knockoutjs构建Web应用程序,mvvm,jquery-mobile,knockout.js,Mvvm,Jquery Mobile,Knockout.js,我想建立一个移动应用程序,只从html/css和JavaScript中酝酿出来。虽然我对如何使用JavaScript构建web应用程序有相当的了解,但我想我可能会研究一下像jquery mobile这样的框架 起初,我认为jquerymobile只是一个面向移动浏览器的小部件框架。非常类似于jquery ui,但适用于移动世界。但我注意到jquerymobile远不止这些。它附带了一系列的架构,让我们用声明式html语法创建应用程序。因此,对于最容易思考的应用程序,您不需要自己编写一行JavaS

我想建立一个移动应用程序,只从html/css和JavaScript中酝酿出来。虽然我对如何使用JavaScript构建web应用程序有相当的了解,但我想我可能会研究一下像jquery mobile这样的框架

起初,我认为jquerymobile只是一个面向移动浏览器的小部件框架。非常类似于jquery ui,但适用于移动世界。但我注意到jquerymobile远不止这些。它附带了一系列的架构,让我们用声明式html语法创建应用程序。因此,对于最容易思考的应用程序,您不需要自己编写一行JavaScript(这很酷,因为我们都喜欢少工作,不是吗?)

为了支持使用声明性html语法创建应用程序的方法,我认为将jquery mobile与knockoutjs相结合是一个不错的选择。Knockoutjs是一个客户端MVVM框架,旨在将WPF/Silverlight中的MVVM超级功能带到JavaScript世界

对我来说,MVVM是一个新世界。虽然我已经读了很多关于它的书,但我自己以前从未真正使用过它

因此,本文是关于如何使用jquery mobile和knockoutjs构建一个应用程序的。我的想法是写下我在看了几个小时后想到的方法,并让jquery mobile/knockout yoda对其进行评论,告诉我为什么它很糟糕,为什么我不应该首先进行编程;-)

html文件

jquerymobile很好地提供了页面的基本结构模型。虽然我很清楚我可以在以后通过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>
App.js是我的应用程序的入口点。它创建App对象,并为视图模型(很快就会出现)提供名称空间。它列出jquery mobile提供的mobileinit事件

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

我还为主页连接了pagecreate事件,在该事件中,我创建了viewModel的一个实例,以获得传入的服务实例。这一点对我很重要。如果有人认为,这应该是不同的做法,请分享你的想法

关键是,视图模型需要对服务(GetTour/、SaveTour等)进行操作。但是我不想让ViewModel知道更多关于它的信息。例如,在我们的例子中,我只是传入一个模拟的ajax服务,因为后端还没有开发

我应该提到的另一件事是ViewModel对实际视图一无所知。这就是为什么我在pagecreate处理程序中调用ko.applyBindings(viewModel,this)。我想将视图模型与实际视图分开,以便更容易测试它

App.ViewModels.HomeScreenViewModel.js

虽然您会发现大多数knockoutjs视图模型示例使用的是对象文字语法,但我使用的是“self”helper对象的传统函数语法。基本上,这是一个品味的问题。但是当你想让一个可观察属性引用另一个时,你不能一次写下对象文字,这会使它不那么对称。这就是我选择不同语法的原因之一

第二个原因是我可以像前面提到的那样作为参数传递的服务

这个视图模型还有一点我不确定我是否选择了正确的方法。我希望定期轮询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)
好的,我写了很多我最初计划写的东西。我的手指受伤了,我的狗要我带它们去散步,我感到筋疲力尽。我确信这里遗漏了很多东西,而且我还犯了很多拼写错误和语法错误。如果有什么不清楚的地方,对我大喊大叫,我会在以后更新帖子

这个帖子看起来可能不是一个问题,但实际上是一个问题!我想让你分享你对我的方法的想法,如果你认为它是好的还是坏的,或者如果我遗漏了什么

更新

由于这篇文章受到了广泛的欢迎,并且有几个人要求我这样做,我将这个示例的代码放在github上:

趁热去拿

注意:从jQuery 1.7开始,该方法已被弃用。用于附加事件处理程序。jQuery旧版本的用户应优先使用

我也在做同样的事情(击倒+jquerymobile)。我正试图写一篇关于我所学到的东西的博文,但同时这里有一些建议。记住,我也在尝试学习knockout/jquerymobile

查看模型和页面 每个jQuery移动页面仅使用一(1)个视图模型对象。否则,多次触发的单击事件可能会出现问题

查看模型并单击 仅对视图模型单击事件使用ko.observable-fields

应用绑定
(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)
(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)
$('#field').live('pagehide', function() {
    ko.cleanNode($('#field')[0]);
});
$('[data-role="page"]').live('pagehide', function() {
    $('.ui-btn-active').removeClass('ui-btn-active');
});