如何使用 jquery-mobile 和 Knockoutjs 构建 web 应用程序 [英] How to architecture a webapp using jquery-mobile and knockoutjs

查看:25
本文介绍了如何使用 jquery-mobile 和 Knockoutjs 构建 web 应用程序的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想构建一个移动应用程序,除了 html/css 和 JavaScript 之外别无他物.虽然我对如何使用 JavaScript 构建网络应用程序有一定的了解,但我想我可能会研究一下像 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 对其进行评论,向我展示为什么它很糟糕以及为什么我不应该首先进行编程地方;-)

html

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

<头><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><身体><!-- 第一页开始--><div data-role="page" id="home"><div data-role="header"><h1>演示应用</h1></div><!--/header --><div data-role="内容"><div class="ui-grid-a"><div class="ui-block-a"><div class="ui-bar" style="height:120px"><h1>今日游览(请等待10秒看效果)</h1><p><span data-bind="text: toursTotal"></span>总计</p><p><span data-bind="text: toursRunning"></span>运行

<p><span data-bind="text: toursCompleted"></span>完成</p>

<fieldset class="ui-grid-a"><div class="ui-block-a"><button data-bind="click: showTourList, jqmButtonEnabled: toursAvailable" data-theme="a">旅游列表</button></div></fieldset></div><!--/content --><div data-role="footer" data-position="fixed"><h4>克里斯托夫·伯格多夫</h4></div><!--/header --></div><!--/page --><!-- 旅游列表页面--><div data-role="page" id="tourlist"><div data-role="header"><h1>条形</h1></div><!--/header --><div data-role="内容"><p><a href="#home">返回首页</a></p></div><!--/content --><div data-role="footer" data-position="fixed"><h4>克里斯托夫·伯格多夫</h4></div><!--/header --></div><!--/page -->

JavaScript

那么让我们来到有趣的部分——JavaScript!

当我开始考虑对应用程序进行分层时,我想到了几件事(例如可测试性、松散耦合).我将向您展示我如何决定拆分我的文件并评论诸如为什么我在去的时候选择一件事而不是另一件事......

App.js

var App = window.App = {};App.ViewModels = {};$(document).bind('mobileinit', function(){//当应用程序运行时使用 App.Service.mockStatistic({ToursCompleted: 45});从控制台伪造后端数据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 = 功能(服务){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.间隔(10000).Select(service.getStatistics).转变().订阅(功能(统计){self.toursTotal(statistics.ToursTotal);self.toursRunning(statistics.ToursRunning);self.toursCompleted(statistics.ToursCompleted);});};self.stopServicePolling = DisposableServicePoller.Dispose;回归自我;};})(应用程序)

虽然您会发现大多数 Knockoutjs 视图模型示例都使用对象字面量语法,但我使用的是带有self"辅助对象的传统函数语法.基本上,这是一个品味问题.但是当你想要一个可观察的属性来引用另一个时,你不能一次性写下对象字面量,这会降低它的对称性.这就是我选择不同语法的原因之一.

下一个原因是我之前提到的可以作为参数传递的服务.

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

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

App.MockedStatisticsService.js

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

(function(App){App.MockedStatisticService = function(){var self = {},默认统计= {旅游总数:505,ToursRunning: 110,游览已完成:115},currentStatistic = $.extend({}, defaultStatistic);;self.mockStatistic = 函数(统计){currentStatistic = $.extend({}, defaultStatistic, statistics);};self.getStatistics = function(){var asyncSubject = new Rx.AsyncSubject();asyncSubject.OnNext(currentStatistic);asyncSubject.OnCompleted();返回 asyncSubject.AsObservable();};回归自我;};})(应用程序)

好的,我写的比我最初计划写的要多得多.我的手指受伤了,我的狗叫我带它们去散步,我感到筋疲力尽.我确信这里遗漏了很多东西,并且我输入了一堆拼写错误和语法错误.如果有什么不明白的可以对我大喊大叫,我稍后会更新帖子.

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

更新

由于这篇文章的人气很高,也因为有几个人要求我这样做,我把这个例子的代码放在了github上:

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

趁热吃吧!

解决方案

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

我正在做同样的事情(淘汰赛 + jquery mobile).我正在尝试写一篇关于我所学知识的博客文章,但同时这里有一些提示.请记住,我也在努力学习淘汰赛/jquery 移动版.

视图模型和页面

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

查看模型并点击

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

ko.applyBinding 一次

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

pagehide 和 ko.cleanNode

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

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

页面隐藏并点击

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

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

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!

解决方案

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

这篇关于如何使用 jquery-mobile 和 Knockoutjs 构建 web 应用程序的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
其他开发最新文章
热门教程
热门工具
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆