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

发布于 2024-11-09 12:25:58 字数 7728 浏览 0 评论 0原文

我想构建一个移动应用程序,只用 html/css 和 JavaScript 编写。虽然我对如何使用 JavaScript 构建 Web 应用程序有相当的了解,但我想我可能会研究一下像 jquery-mobile 这样的框架。

起初,我认为 jquery-mobile 只是一个针对移动浏览器的小部件框架。与 jquery-ui 非常相似,但适用于移动世界。但我注意到 jquery-mobile 的功能远不止于此。它附带了一堆架构,让您可以使用声明性 html 语法创建应用程序。 程序,您不需要自己编写一行 JavaScript(这很酷,因为我们都喜欢减少工作量,不是吗?)

因此,对于最简单的应用 声明性 html 语法,我认为将 jquery-mobile 与 knockoutjs 结合起来是一个很好的选择。 Knockoutjs 是一个客户端 MVVM 框架,旨在将 WPF/Silverlight 中众所周知的 MVVM 超能力引入 JavaScript 世界。

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

所以这篇文章是关于如何一起使用 jquery-mobile 和 knockoutjs 构建应用程序的。我的想法是写下我看了几个小时后想到的方法,并让一些 jquery-mobile/knockout yoda 对其进行评论,向我展示为什么它很糟糕以及为什么我不应该首先进行编程place ;-)

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 对象并为视图模型提供命名空间(即将推出)。它监听 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)

虽然您会发现大多数 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)

好吧,我按照最初计划写了很多。我的手指受伤了,我的狗要我带它们去散步,我感到筋疲力尽。我确信这里遗漏了很多东西,并且我添加了一堆打字错误和语法错误。如果有不清楚的地方请对我大喊大叫,我稍后会更新帖子。

该帖子可能看起来不是一个问题,但实际上它是一个问题!我希望您能分享您对我的方法的看法,以及您认为它是好是坏,或者我是否遗漏了一些东西。

更新

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

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

趁热获取!

I would like to build a mobile app, brewed from nothing more but html/css and JavaScript. While I have a decent knowledge of how to build a web app with JavaScript, I thought I might have a look into a framework like jquery-mobile.

At first, I thought jquery-mobile was nothing more then a widget framework which targets mobile browsers. Very similar to jquery-ui but for the mobile world. But I noticed that jquery-mobile is more than that. It comes with a bunch of architecture and let's you create apps with a declarative html syntax. So for the most easy thinkable app, you wouldn't need to write a single line of JavaScript by yourself (which is cool, because we all like to work less, don't we?)

To support the approach of creating apps using a declarative html syntax, I think it's a good take to combine jquery-mobile with knockoutjs. Knockoutjs is a client-side MVVM framework that aims to bring MVVM super powers known from WPF/Silverlight to the JavaScript world.

For me MVVM is a new world. While I have already read a lot about it, I have never actually used it myself before.

So this posting is about how to architecture an app using jquery-mobile and knockoutjs together. My idea was to write down the approach that I came up with after looking at it for several hours, and have some jquery-mobile/knockout yoda to comment it, showing me why it sucks and why I shouldn't do programming in the first place ;-)

The html

jquery-mobile does a good job providing a basic structure model of pages. While I am well aware that I could have my pages to be loaded via ajax afterwards, I just decided to keep all of them in one index.html file. In this basic scenario we are talking about two pages so that it shouldn't be too hard to stay on top of things.

<!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>

The JavaScript

So let's come to the fun part - the JavaScript!

When I started to think about layering the app, I have had several things in mind (e.g. testability, loose coupling). I'm going to show you how I decided to split of my files and comment things like why did I choose one thing over another while I go...

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 is the entry point of my app. It creates the App object and provides a namespace for the view models (soon to come). It listenes for the mobileinit event which jquery-mobile provides.

As you can see, I'm creating a instance of some kind of ajax service (which we will look at later) and save it to the variable "service".

I also hook up the pagecreate event for the home page in which I create an instance of the viewModel that gets the service instance passed in. This point is essential to me. If anybody thinks, this should be done differently, please share your thoughts!

The point is, the view model needs to operate on a service (GetTour/, SaveTour etc.). But I don't want the ViewModel to know any more about it. So for example, in our case, I'm just passing in a mocked ajax service because the backend hasn't been developed yet.

Another thing I should mention is that the ViewModel has zero knowledge about the actual view. That's why I'm calling ko.applyBindings(viewModel, this) from within the pagecreate handler. I wanted to keep the view model seperated from the actual view to make it easier to test it.

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)

While you will find most knockoutjs view model examples using an object literal syntax, I'm using the traditional function syntax with a 'self' helper objects. Basically, it's a matter of taste. But when you want to have one observable property to reference another, you can't write down the object literal in one go which makes it less symmetric. That's one of the reason why I'm choosing a different syntax.

The next reason is the service that I can pass on as a parameter as I mentioned before.

There is one more thing with this view model which I'm not sure if I did choose the right way. I want to poll the ajax service periodically to fetch the results from the server. So, I have choosen to implement startServicePolling/stopServicePolling methods to do so. The idea is to start the polling on pageshow, and stop it when the user navigates to different page.

You can ignore the syntax which is used to poll the service. It's RxJS magic. Just be sure I'm polling it and update the observable properties with the returned result as you can see in the Subscribe(function(statistics){..}) part.

App.MockedStatisticsService.js

Ok, there is just one thing left to show you. It's the actual service implementation. I'm not going much into detail here. It's just a mock that returns some numbers when getStatistics is called. There is another method mockStatistics which I use to set new values through the browsers js console while the app is running.

(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, I wrote much more as I initially planned to write. My finger hurt, my dogs are asking me to take them for a walk and I feel exhausted. I'm sure there are plenty things missing here and that I put in a bunch of typos and grammer mistakes. Yell at me if something isn't clear and I will update the posting later.

The posting might not seem as an question but actually it is! I would like you to share your thoughts about my approach and if you think it's good or bad or if I'm missing out things.

UPDATE

Due to the major popularity this posting gained and because several people asked me to do so, I have put the code of this example on github:

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

Get it while it's hot!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

下壹個目標 2024-11-16 12:25:58

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


我正在做同样的事情(knockout + 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');
});

Note: As of jQuery 1.7, the .live() method is deprecated. Use .on() to attach event handlers. Users of older versions of jQuery should use .delegate() in preference to .live().

I'm working on the same thing (knockout + jquery mobile). I'm trying to write a blog post about what I've learned but here are some pointers in the meantime. Remember that I'm also trying to learn knockout/jquery mobile.

View-Model and Page

Only use one (1) view-model object per jQuery Mobile-page. Otherwise you can get problems with click-events that are triggered multiple times.

View-Model and click

Only use ko.observable-fields for view-models click-events.

ko.applyBinding once

If possible: only call ko.applyBinding once for every page and use ko.observable’s instead of calling ko.applyBinding multiple times.

pagehide and ko.cleanNode

Remember to clean up some view-models on pagehide.
ko.cleanNode seems to disturb jQuery Mobiles rendering - causing it to re-render the html. If you use ko.cleanNode on a page you need to remove data-role’s and insert the rendered jQuery Mobile html in the source code.

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

pagehide and click

If you are binding to click-events - remember to clean up .ui-btn-active. The easiest way to accomplish this is using this code snippet:

$('[data-role="page"]').live('pagehide', function() {
    $('.ui-btn-active').removeClass('ui-btn-active');
});
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文