jquery-mobile 및 knockoutjs를 사용하여 웹앱을 아키텍처하는 방법
html / css와 자바 스크립트로 만든 모바일 앱을 만들고 싶습니다. JavaScript로 웹 앱을 빌드하는 방법에 대한 적절한 지식을 가지고 있지만 jquery-mobile과 같은 프레임 워크를 살펴볼 수 있다고 생각했습니다.
처음에는 jquery-mobile이 모바일 브라우저를 대상으로하는 위젯 프레임 워크에 불과하다고 생각했습니다. jquery-ui와 매우 유사하지만 모바일 세계에 적합합니다. 그러나 나는 jquery-mobile이 그 이상이라는 것을 알았습니다. 많은 아키텍처와 함께 제공되며 선언적 HTML 구문으로 앱을 만들 수 있습니다. 따라서 가장 쉽게 생각할 수있는 앱의 경우 한 줄의 자바 스크립트를 직접 작성할 필요가 없습니다 (우리 모두가 덜 일하는 것을 좋아하기 때문에 멋지죠?)
선언적 html 구문을 사용하여 앱을 만드는 접근 방식을 지원하려면 jquery-mobile과 knockoutjs를 결합하는 것이 좋습니다. Knockoutjs는 WPF / Silverlight에서 알려진 MVVM 슈퍼 파워를 JavaScript 세계로 가져 오는 것을 목표로하는 클라이언트 측 MVVM 프레임 워크입니다.
저에게 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>
자바 스크립트
이제 재미있는 부분 인 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 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');
});
'developer tip' 카테고리의 다른 글
그룹화 된 Pandas DataFrame에 함수를 병렬로 효율적으로 적용 (0) | 2020.09.10 |
---|---|
지구가 아닌 달력 구현 (0) | 2020.09.10 |
기본 응용 프로그램 및 매개 변수가있는 C # 파일 열기 (0) | 2020.09.10 |
AngularJS $ http, CORS 및 http 인증 (0) | 2020.09.10 |
Xcode 8 프로비저닝 프로파일이 다운로드되지 않음 (0) | 2020.09.10 |