如何使用jQuery-Mobile和Knockout.js构建Web应用程序


88

我想构建一个移动应用程序,仅用html / css和JavaScript编写。虽然我对如何使用JavaScript构建Web应用程序有相当的了解,但我认为我可以研究一下jquery-mobile之类的框架。

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

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

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

因此,这篇文章是关于如何一起使用jquery-mobile和kickoutjs构建应用程序的。我的想法是写下看了几个小时后想到的方法,并用一些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>

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();
  });
});

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

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

我也连接了页面主页事件,在该事件中,我创建了一个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)

尽管您会发现大多数使用对象文字语法的基因敲除视图模型示例,但我使用的是带有“自我”帮助对象的传统函数语法。基本上,这是一个品味问题。但是,当您想拥有一个可观察的属性来引用另一个属性时,就无法一次性写下对象文字,这会使它的对称性降低。这就是我选择其他语法的原因之一。

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

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

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

App.MockedStatisticsService.js

好的,只剩下一件事要告诉你。这是实际的服务实现。我在这里不做详细介绍。这只是一个模拟,在调用getStatistics时会返回一些数字。应用程序运行时,还有另一种方法嘲笑统计信息,可用于通过浏览器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上:

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

趁热拿到!


7
我不确定人们是否有足够具体的问题要解决。我喜欢您在此处提供的详细信息,但似乎可以将其用于讨论。用更少的词:“好博客”;)
Bernhard Hofmann

我很高兴你喜欢它。我担心的是我写的太多了,以至于人们害怕写一个简短的答案。但是,欢迎进行任何讨论。如果stackoverflow是开始讨论的错误位置,我们可以切换到Google 网上论坛
Christoph

嗨克里斯托夫,您如何看待这种方法?
hkon 2011年

实际上,我转到了更出色的AngularJS框架;-)
Christoph

1
如果仅保留前几段作为问题,然后将其余部分移至自我回答,则可能会更好。
rjmunro 2012年

Answers:


30

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

我正在做同样的事情(淘汰赛+ jQuery mobile)。我正在尝试写一篇关于我所学到的博文,但与此同时,这里有一些提示。记住,我也在尝试学习淘汰赛/ jQuery Mobile。

视图模型和页面

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

查看模型并单击

仅将ko.observable-fields用于视图模型的单击事件。

ko.applyBinding一次

如果可能的话:每页只调用一次ko.applyBinding,并使用ko.observable而不是多次调用ko.applyBinding。

pagehide和ko.cleanNode

记住要在pagehide上清理一些视图模型。ko.cleanNode似乎干扰了jQuery Mobiles的呈现-导致其重新呈现html。如果在页面上使用ko.cleanNode,则需要删除数据角色并将插入的jQuery Mobile html插入源代码中。

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

pagehide,然后单击

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

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

由于我的问题非常明确,您是最愿意回答的人,因此我将让您接受。
Christoph

你有没有解决这个问题?我真不愿意将KO和JQM集成在一起,也没有很好的指导(或演示端到端演示的jsFiddle)。
2012年

1
不,我转到AngularJS框架。我发现这比KO优越。有一个相当不错的适配器项目可以使AngularJS / jqm永远成为最好的朋友:github.com/tigbro/jquery-mobile-angular-adapter但是,到目前为止,我所做的似乎使用该适配器已经过头了。毕竟,仅使用jqm的html / css并将控件转换为Angular指令就非常容易了:jsfiddle.net/zy7Rg/7
Christoph

您可以创建一个我在这里定义的结构。我敢肯定,这样您将完全控制该应用程序。
Muhammad Raheel 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.