Framework7、RequireJS、Handlebarsを使用したモバイルJavascript MVCアプリケーション開発

最近、iPhoneとAndroidアプリケーションの開発という課題に直面しました。 iOSの開発経験がなかったので、一度書いて両方のプラットフォームで実行したかった。 したがって、JavascriptとPhoneGapが選択されました。



また、言語を比較的迅速に決定した場合、多くの質問がありました。

IOS7インターフェースを可能な限り繰り返し、アプリケーションの速度をネイティブのようにしたかったのです。 同時に、一方では、dojoやjquery mobileに似た「モンスター」を使用したいという欲求もありませんでした。 一方、便利なモジュラーMVCアプリケーション構造を取得したかったのです。



その結果、私の個人的な比較の決勝戦は次のようになりました。

- イオンフレームワーク: http : //ionicframework.com/

-Framework7: http : //www.idangero.us/framework7/



Ionikは最初、ドキュメント、簡単な例、AngularJsの使い慣れたコード構造が好きでした。 しかし、アプリケーションを最初に作成しようとした後、失望がありました。 Iphone5で起動されたシンプルなアプリケーションは遅かった。 ボタンまたはナビゲーションをクリックすると、押してからトリガーするまでの間に視覚的に遅延が目立ちました。 クリック時の300ミリ秒の遅延に似ています。 しかし、作成者によると、彼らのフレームワークには、fastclickライブラリ...ストレンジの独自の実装が含まれています。 また、単純なアプリケーションであっても、アニメーションのスローダウンは時々顕著でした。 その結果、ドキュメントとテストケースを数日間読んだ後、他の何かを探す必要があることに気付きました。



その後、Framework7に戻りました。 テストアプリケーションを起動し、キッチンシンクのコンポーネントを見て、最初はすごい効果を経験しました。 iPhoneでは、すべてが高速で美しく、ネイティブに非常に似ています。 同時に、次の2つの大きな欠点に直面しました。



一般に、私は理論的な知識を引き出し、さまざまな記事や例を見て、モバイルアプリケーションを作成するためのFramework7とモジュラーMVCアプローチを組み合わせる問題を解決することができました。 モジュールの非同期ロードを実装するために、RequireJsをテンプレート(ハンドルバー)に使用しました。



したがって、私はいくつかのケーススタディを作成し、コミュニティで共有したいと考えています。 初心者の開発者と、このフレームワークをまだ知らない経験豊富な開発者の両方に役立つことを願っています。



はじめに



作業には、次のライブラリが必要です。





プロジェクト構造





プロジェクトファイルの次の構造を作成します(index.htmlおよびapp.jsファイルは今のところ空のままにします)

あなたの人生を簡素化するために、次のリンクの構造を持つアーカイブをダウンロードできます。

Dropbox

(index.htmlおよびapp.jsファイルの最初のバージョンは、すでにこのアーカイブに書き込まれています)



また、すぐにGithubのソースへのリンクを提供します-最新バージョンと段階的な編集履歴があります-このテストアプリケーションの作成:

https://github.com/philipshurpik/Framework7-MVC-base


最も簡単なindex.htmlファイルを作成して、必要なすべてのライブラリを接続します。



<!DOCTYPE html> <html class="with-statusbar-overlay"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no, minimal-ui"> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> <title>F7 Contacts MVC</title> <link rel="stylesheet" href="lib/css/framework7.css"> <link rel="stylesheet" href="lib/css/ionicons.css"> <link rel="stylesheet" href="css/app.css"> </head> <body> <div class="statusbar-overlay"></div> <div class="views"> <div class="view view-main navbar-fixed"> <div class="navbar"> <div class="navbar-inner"> <div class="left"></div> <div class="center" style="left:22px">Contacts</div> <div class="right"> <a href="contact.html" class="link icon-only"><i class="icon icon-plus">+</i></a> </div> </div> </div> <div class="pages"> <div data-page="list" class="page"> <div class="page-content"> <div class="list-block contacts-list"> <ul> <a href="contact.html" class="item-link item-content"> <div class="item-media"><i class="icon ion-ios7-person"></i></div> <div class="item-inner"> <div class="item-title">Andrey Smirnov</div> </div> </a> <a href="contact.html?id={{id}}" class="item-link item-content"> <div class="item-media"><i class="icon ion-ios7-person"></i></div> <div class="item-inner"> <div class="item-title">Olga Kot</div> </div> </a> </ul> </div> </div> </div> </div> </div> </div> </body> </html> <script type="text/javascript" src="lib/framework7.js"></script> <script type="text/javascript" src="app.js"></script>
      
      







また、app.jsファイルにアプリケーションの初期化を配置します。

 var f7 = new Framework7({ modalTitle: 'F7-MVC-Base', animateNavBackIcon: true }); var mainView = f7.addView('.view-main', { dynamicNavbar: true });
      
      







次の図を実行して取得します。



ここ。 最初のページがあり、その上にhello-world以上のものがあります。



はい、誰も知らない場合。 Devtools Chromeのコンソールの横にある[エミュレーション]タブでは、目的のデバイスを選択し、このデバイスの画面でアプリケーションがどの程度表示されるかを確認できます。






RequireJとハンドルバーを接続し、コンタクトをロードします



次に、連絡先を動的に(たとえばlocalstorageから)ロードし、リストに表示する必要があります。

これを行うには、ファイルを変更します。



1. index.html

app.jsファイルの直接接続をRequire.Js接続に置き換えます

 <script data-main="app" src="lib/require.js"></script>
      
      



data-main属性は、アプリケーションへのエントリポイントを指します(これはapp.jsファイルです)&

ulタグの内側にあるものを削除することもできます-リストの内側はテンプレートを使用して生成されます。



2. app.js

RequireJsモジュールでファイルをやり直します。

 define('app', ['js/list/listController'], function(listController) { var f7 = new Framework7({ modalTitle: 'F7-MVC-Base', animateNavBackIcon: true }); var mainView = f7.addView('.view-main', { dynamicNavbar: true }); listController.init(); return { f7: f7, mainView: mainView }; });
      
      





すべて同じですが、モジュールにラップされただけで、まだ利用できない最初のコントローラーのダウンロードが追加されました。



メインページ:コントローラー、ビュー、要素テンプレート



次に、メインページ、そのプレゼンテーション、およびハンドルバーテンプレート用のコントローラーを作成する必要があります。

次のようにファイルに名前を付けて配置することを提案します。



はい、そのようなグループ化は、機能の面で、ビュー、モデル、コントローラーを別のディレクトリに配置するよりも、プロジェクトでははるかに便利なようです。


リスト用のシンプルなコントローラーを作成します。 その中で、すぐにlocalstorageをいくつかの連絡先オブジェクトで初期化します:



ファイル:js / list / listController.js

 define(["js/list/listView"], function(ListView) { function init() { var contacts = loadContacts(); ListView.render({ model: contacts }); } function loadContacts() { var f7Base = localStorage.getItem("f7Base"); var contacts = f7Base ? JSON.parse(f7Base) : tempInitializeStorage(); return contacts; } function tempInitializeStorage() { var contacts = [ {id: "1", firstName: "Alex", lastName: "Black", phone: "+380501234567" }, {id: "2", firstName: "Kate", lastName: "White", phone: "+380507654321" } ]; localStorage.setItem("f7Base", JSON.stringify(contacts)); return JSON.parse(localStorage.getItem("f7Base")); } return { init: init }; });
      
      







また、テンプレートを使用してデータ(初期化時に渡す)のレンダリングを行うビューを追加する必要があります。

ファイル:js / list / listView.js

 define(['hbs!js/list/contact-list-item'], function(template) { var $ = Framework7.$; function render(params) { $('.contacts-list ul').html(template(params.model)); } return { render: render }; });
      
      







また、単純なテンプレートのコード:

ファイル:js / list / contact-list-item.hbs

 {{#.}} <a href="contact.html?id={{id}}" class="item-link item-content"> <div class="item-media"><i class="icon ion-ios7-person"></i></div> <div class="item-inner"> <div class="item-title">{{firstName}} {{lastName}}</div> </div> </a> {{/.}}
      
      







私たちはすべてを開始しますが、モジュール化され、はるかに拡張可能です。



次に、連絡先を表示および編集するためのページを追加する必要があります。



Framework7のページ間の閲覧



各ページは個別のhtmlファイルに配置されます。

ページはdiv c class =” page”内に含まれています

 <div class="page" data-page="list">
      
      





data-page属性は、今後ルーティングに必要となる一意のページ名を定義します。

ページのすべての視覚要素を内部に配置する必要があります。

 <div class="page-content">     <div class="page">
      
      





ページ間のナビゲーションは、HTMLリンクをクリックして実行されます。

 <a href="about.html">Go to About page</a>
      
      



jsコードから:

 app.mainView.loadPage('about.html');
      
      





(アニメーションとともに)戻るナビゲーションも同様に実行されます。

または、backクラスをリンクに追加することにより:

 <a href="index.html" class="back"> Go back to home page </a>
      
      



または、jsコードから:

 app.mainView.goBack();
      
      





ページを切り替えると、Framework7は次のサブスクライブ可能なイベントを生成します。

PageBeforeInit、PageInit、PageBeforeAnimation、PageAfterAnimation、PageBeforeRemove



ここでページとイベントの完全な情報:

http://www.idangero.us/framework7/docs/pages.html

http://www.idangero.us/framework7/docs/linking-pages.html



router.jsを作成する



DOMに新しいページ-PageBeforeInitを挿入した後に発生するイベントを使用します。

簡単なルーター(router.jsファイル)を作成し、jsフォルダーに配置します。このフォルダーでpageBeforeInitイベントをサブスクライブします。



 define(function() { var $ = Framework7.$; function init() { $(document).on('pageBeforeInit', function (e) { var page = e.detail.page; load(page.name, page.query); }); } function load(controllerName, query) { require(['js/' + controllerName + '/'+ controllerName + 'Controller'], function(controller) { controller.init(query); }); } return { init: init, load: load }; });
      
      





イベントがトリガーされると、Requireを使用して必要なコントローラーモジュールをロードして初期化し、ページが開かれたリクエストパラメーターを渡します。



app.jsモジュールを再実行し、ルーターの初期化を追加して、コントローラーの接続と初期化を削除します。

 define('app', ['js/router'], function(Router) { Router.init(); var f7 = new Framework7({ modalTitle: 'F7-MVC-Base', animateNavBackIcon: true }); var mainView = f7.addView('.view-main', { dynamicNavbar: true }); return { f7: f7, mainView: mainView, router: router }; });
      
      





これで、メインページをDOMに挿入した後、最初にアプリケーションをロードすると、pageBeforeInitイベントハンドラーが発生します。

同時に、そのe.detail.page.nameプロパティはlist、つまり、ここでdata-pageプロパティで設定されたものと等しくなります。したがって、対応するコントローラーが起動されます。



連絡先編集ページ



次に、連絡先を追加および編集するためのページを作成する必要があります。

contact.htmlファイルをhtmlプロジェクトのルートに追加します(ファイル構造をアーカイブからダウンロードした場合、既にそこにあるはずです)

contact.htmlへの対応するリンクは、メインページのnavbarおよび連絡先リスト要素のリストに既に追加されています。

 <div class="navbar"> <div class="navbar-inner"> <div class="left sliding"> <a href="#" class="back link"> <i class="icon icon-back-white"></i> <span>Back</span> </a> </div> <div class="center contacts-header"></div> <div class="right contact-save-link"> <a href="#" class="link"> <span>Save</span> </a> </div> </div> </div> <div class="pages"> <div data-page="contact" class="page contact-page"> </div> </div>
      
      







これで、リスト項目または追加ボタンをクリックすると、ルーターはjs / contact / contactControllerファイルをダウンロードしようとします。



したがって、ページのプレゼンテーションとページのテンプレートコンテンツを作成する必要があります。 このように:





contactController.jsファイルの内容:

 define(["app","js/contact/contactView"], function(app, ContactView) { var state = {isNew: false}; var contact = null; function init(query){ if (query && query.id) { var contacts = JSON.parse(localStorage.getItem("f7Base")); for (var i = 0; i< contacts.length; i++) { if (contacts[i].id === query.id) { contact = contacts[i]; state.isNew = false; break; } } } else { contact = { id: Math.floor((Math.random() * 100000) + 5).toString()}; state.isNew = true; } ContactView.render({ model: contact, state: state }); } return { init: init }; });
      
      





ページが編集モードの場合(クエリに連絡先ID値が含まれている場合、localStorageから取得します)。

そうでない場合は、新しいものを作成します。 これまでのところ、簡単にするために、モデルは使用していません。そのため、連絡先は単なるオブジェクトです。



contactView.jsビューページ:

 define(['hbs!js/contact/contact'], function(viewTemplate) { var $ = Framework7.$; function render(params) { $('.contact-page').html(viewTemplate({ model: params.model })); $('.contacts-header').text(params.state.isNew ? "New contact" : "Contact"); } return { render: render } });
      
      





そして、contact.hbsテンプレート:

 <div class="page-content"> <form id="contactEdit" class="list-block"> <ul> <input name="id" type="hidden" value="{{model.id}}"> <li> <div class="item-content"> <div class="item-media"><i class="icon ion-ios7-football-outline"></i></div> <div class="item-inner"> <div class="item-input"> <input name="firstName" type="text" placeholder="First name" value="{{model.firstName}}"> </div> </div> </div> </li> <li> <div class="item-content"> <div class="item-media"><i class="icon ion-ios7-football-outline"></i></div> <div class="item-inner"> <div class="item-input"> <input name="lastName" type="text" placeholder="Last name" value="{{model.lastName}}"> </div> </div> </div> </li> <li> <div class="item-content"> <div class="item-media"><i class="icon ion-ios7-telephone-outline"></i></div> <div class="item-inner"> <div class="item-input"> <input name="phone" type="tel" placeholder="Phone" value="{{model.phone}}"> </div> </div> </div> </li> </ul> </form> </div>
      
      





じゃあ これで、連絡先を追加または編集するためのページを開くことができます。





連絡先を保存および削除する機能を追加する必要があります。

保存することから始めましょう。



連絡先を保存する



まず、保存ボタンハンドラーを追加します。

もちろん、次のようにコントローラーで直接これを行うことができます。

 $('.contact-save-link').on('click', function() { // some code here });
      
      





ただし、これは良くありません。DOMを使用して作業を分離し、データとモデルを使用することをお勧めします。

したがって、イベント処理のサブスクリプションと処理自体を分離します。

コントローラーでは、バインディングの配列を作成します。

 var bindings = [{ element: '.contact-save-link', event: 'click', handler: saveContact }];
      
      





この配列をparamsオブジェクトのプロパティの1つとしてビューに渡しましょう。



そして、ハンドラー関数を追加します。

 function saveContact() { // some code here }
      
      





そして、ビューで、この構成のイベントにバインドを追加します-bindEvents関数:

  function bindEvents(bindings) { for (var i in bindings) { $(bindings[i].element).on(bindings[i].event, bindings[i].handler); } }
      
      





そして、render関数からの彼女の呼び出し:

 bindEvents(params.bindings);
      
      





次に、フォームに入力されたデータの値を取得する必要があります。

saveContact関数でこれを行います。



 function saveContact() { var contacts = JSON.parse(localStorage.getItem("f7Base")) var newContact = app.f7.formToJSON('#contactEdit'); if (state.isNew) { contacts.push(newContact) } else { for (var i = 0; i< contacts.length; i++) { if (contacts[i].id === newContact.id) { contacts[i] = newContact; break; } } } localStorage.setItem("f7Base", JSON.stringify(contacts)); app.router.load('list'); app.mainView.goBack(); }
      
      





また、受信したデータをlocalStorageにすぐに保存します。

最後の2行は、前のページ(リスト)に戻るとともに、listControllerにデータを再読み込みします。



今ではすべてが機能しています!



モデル作成:



しかし、コントローラーですべてのデータを処理することはあまり良くありません。 また、データ検証などの特別な機能を追加する必要がある場合があります。



したがって、js / contactModel.jsファイルにモデルを作成します。

1つは、検証関数を追加し、別のオブジェクトの値を設定することです。



 define(['app'],function(app) { function Contact(values) { values = values || {}; this.id = values['id'] || Math.floor((Math.random() * 100000) + 5).toString(); this.firstName = values['firstName'] || ''; this.lastName = values['lastName'] || ''; this.phone = values['phone'] || ''; } Contact.prototype.setValues = function(formInput) { for(var field in formInput){ if (this[field] !== undefined) { this[field] = formInput[field]; } } }; Contact.prototype.validate = function() { var result = true; if (!this.firstName && !this.lastName) { result = false; } return result; }; return Contact; });
      
      





関数はオブジェクト自体には追加されず、プロトタイプに追加されることに注意してください。 したがって、オブジェクトを転送または保存すると、そのプロパティのみが関数なしでJSONに転送されます。



次に、モデルをcontactControllerに接続します。

依存関係のリストに追加します。

 define(["app","js/contact/contactView", "js/contactModel"], function(app, ContactView, Contact)
      
      





init関数では、連絡先の割り当てと作成をそれぞれ変更します。

 contact = new Contact(contacts[i]);
      
      





そして

 contact = new Contact();
      
      





そして、モデル検証の実行を追加することにより、保存機能を変更します。

 function saveContact() { var formInput = app.f7.formToJSON('#contactEdit'); contact.setValues(formInput); if (!contact.validate()) { app.f7.alert("First name and last name are empty"); return; } var contacts = JSON.parse(localStorage.getItem("f7Base")); if (state.isNew) { contacts.push(contact); } else { for (var i = 0; i< contacts.length; i++) { if (contacts[i].id === contact.id) { contacts[i] = contact; break; } } } localStorage.setItem("f7Base", JSON.stringify(contacts)); app.mainView.goBack(); app.router.load('list'); }
      
      





保存の準備ができました。



スワイプして削除



連絡先リストからの削除を追加するために残ります。

リスト内の削除するスワイプジェスチャーを使用してこれを実装します。

要素テンプレートのレイアウトを変更します。

 {{#.}} <li id="{{id}}" class="swipeout"> <a href="contact.html?id={{id}}" class="item-link item-content swipeout-content"> <div class="item-media"><i class="icon ion-ios7-person"></i></div> <div class="item-inner"> <div class="item-title">{{firstName}} {{lastName}}</div> </div> </a> <div class="swipeout-actions"> <div class="swipeout-actions-inner"> <a href="#" class="swipeout-delete">Delete</a> </div> </div> </li> {{/.}}
      
      





イベントサブスクリプションをlistControllerに追加します。

 var bindings = [{ element: '.swipeout', event: 'deleted', handler: itemDeleted }];
      
      





そして、連絡先のサブスクリプションと同様にそれを行います-ビューに渡し、そこでbindEvents(バインディング)関数にサブスクライブします



また、削除イベントハンドラーも追加します。

 function itemDeleted(e) { var id = e.srcElement.id; var contacts = JSON.parse(localStorage.getItem("f7Base")); for (var i = 0; i < contacts.length; i++) { if (contacts[i].id === id) { contacts.splice(i, 1); } } localStorage.setItem("f7Base", JSON.stringify(contacts)); }
      
      





結果を見てみましょう:





おわりに



Framework7を使用した既製の非常にシンプルなモバイルMVCアプリケーションがあります。

また、Framework7とPhonegapを組み合わせることにより、主にIOS向けの美しいネイティブのようなアプリケーションを作成できます。 これは、ObjectiveCを初めて使用する開発者にとって便利です。

この場合、Android 4.4で完全かつスムーズに動作するクロスプラットフォームアプリケーションがすぐに得られます(将来のバージョンでも同じように動作する可能性が高い)。

以前のバージョンのAndroidで低コストのAndroidデバイスを通常サポートするには、許容可能なUIパフォーマンスを得るためにページ間のアニメーションをオフにするだけで十分です。



プロジェクトソースと編集の一貫した履歴は、ここから入手できます。

https://github.com/philipshurpik/Framework7-MVC-base



また、Framework7のより多くの機能とより多くの機能を使用する連絡先アプリケーションの高度なトレーニング例を作成しました。 左のプルアウトメニューバー、ポップアップ編集、検索バーなどを追加しました。

そのソースは次のとおりです。

https://github.com/philipshurpik/Framework7-Contacts7-MVC

そして、ここにスクリーンショットがあります(シール付き):





これらの例がお役に立てば幸いです。

私自身はこれらについて勉強したので、この記事を作成することにしました。



私は質問に答えてうれしいです。



追伸 このフレームワークvladimirkharlampidiの作成者はまだハブで利用できませんが、トピックがhabrovchanにとって興味深いものであれば、招待を受け入れてディスカッションに参加することも喜んでいると思います。



P.P.S. また、特に古いバージョンでのAndroidの作業速度についても少し調査し、app.cssのリポジトリにcssアニメーションを最適化するためのハックをアップロードしました。 おそらくそれらのいくつかは、フレームワークの将来のバージョンに含まれるでしょう。 まあ、多分誰かが彼らのアプリケーションに役立つでしょう。



All Articles