RequireJSでモジュール管理してみるサンプル(2)
前回の記事で
「RequireJSには、最適化ツール(r.js)を用いて、各モジュールをminifyし結合することができます」
といったので、それについてさくっと書きます。
セットアップ ~ 最適化の実行
前回のサンプルの構成をそのまま最適化していきます。
1.まず、最適化ツール(r.js)の使用にはnode.js環境が必要なため、以下のサイトからインストールします。
Node.js
インストール後、nodeとnpmのバージョンを確認します。
ターミナルから以下のコマンドを実行
node -v npm -v
2.requirejsパッケージをインストールします。
ターミナルから以下のコマンドを実行
npm install -g requirejs
3.実際に最適化を適用します。
ターミナルから以下のコマンドを実行
r.js -o baseUrl=. name=page out=./page-built.js
- baseUrl: jsファイルが含まれているフォルダ、今回はhtmlと同階層
- name: 最終的に読み込むjs(data-mainに指定したjs)を指定
- out: 出力するjsファイル名
4.正常に出力されたら、index.htmlのrequire.jsを読み込むスクリプトタグのdata-mainの指定を出力されたファイルに変更
<script src="require.js" data-main="page-built.js" async></script>
上記で対応は完了です。
実行結果
実行結果は変わりません。
ネットワーク状態をみてみます。
読み込むファイルが1つだけになっています。
モジュールをどんなに分割してもリクエストは増えないのが嬉しいですね。
さくっと書きました。
RequireJSでモジュール管理してみるサンプル
RequireJSとは
Javascriptモジュールローダの1つです。
モジュールの依存管理だけではなく、パフォーマンス面も考慮されたライブラリで、
開発効率とパフォーマンスを両立したJavascript開発を実現できるかも。。。
RequireJSを使わないとこう成り得る...というサンプル
ページのロードが完了したら、タイトルと説明文をページに表示するというサンプルを用意してみました。
敢えて、色々良くない書き方をした部分もありますので、参考にはしないでください。
動作イメージ(in plunker)
ファイル構成
- index.html - メインHTML
- site-common.js - サイト内共通で使う定数や関数をここで定義
- addon.js - 上記のsite-common.jsの機能を拡張するようなモジュール
- page.js - ページのメインコード
各ソース
index.html
site-common.js
addon.js
page.js
あと、このサンプルですが、以下のシナリオで作られたと想定してみてください。
- 最初はシンプルに、ページロード完了時にタイトルと説明を表示していた。
- 後になって、別の開発者がaddon.jsを追加し、page.jsも書き換えて、タイトルに鍵カッコを付けた。
問題点
複数開発者で開発することを考慮すると、とたんに色々な問題点が出てきます。
- ユーザがライブラリの読み込み順を知らないといけない
addon.jsとsite-common.jsの読み込み順を入れ替えると鍵カッコがundefinedになってしまいます。
読み込みの順番が変わると正常動作しないってことはそれを全て把握して読み込む必要が出てきます。めんどい。
- グローバル汚染
定義している変数・関数が、全てグローバル領域に...汚染がひどいです。constantという名前で他の人が変数を定義したら色々死亡します。
- 意図と外れた設計
そもそもconstantへのアクセスは、site-common.jsを見る限り、setConstant,getConstantを用いて行って欲しいという意図が汲み取れますが、addon.jsの開発者がそのことを知らないのか、意図しないconstantへの直接アクセスが行われています。
という訳で、早速RequireJSさんにおでましいただきます。
RequireJSを適用してみる
適用してみた
動作イメージ(in plunker)
動作は同じですね
各ソース
index.html - 修正後
まずは、サイトからrequirejsをDLして設置。
スクリプト読み込みは、このrequire.jsのみに変更します。
data-main属性には、このページのメインコードのみ指定します。
また、scriptタグにasync属性を追加すると、対応ブラウザにおいて非同期でjsを読み込むことができます。
site-common.js - 修正後
まず全体をdefine(function(){ ・・・ })でくくります。
次に関数の戻り値として、このモジュールで公開したいオブジェクトのみを記述します。(return文)
addon.js - 修正後
全体をdefineでくくる際に、第一引数に依存モジュール(ここでは'site-common.js')を指定します。
また、site-common.jsモジュールのアクセスするための引数(名前は何でも良い。ここではCommon)を指定します。
その他は、site-common.jsと同じです。
page.js - 修正後
defineではなくrequireでくくります。
このrequire内部が、ロードが完了した際に実行されますので、window.onloadの内部処理をそのまま書けば大丈夫です。
また、define同様に依存モジュールとその引数も定義します。
考察
RequireJSを適用した事で、どんなメリットが生まれたかを列挙してみます。
- 読み込み時に、順番を意識しなくて良い
読み込みの順番はRequireJSが勝手に解釈してくれるので、各モジュールの依存関係さえ間違っていなければ、意識する必要はありません。
複数人での開発時にも、自分がそのページでやりたいことを書いたファイル(ここではpage.js)をロードすれば問題ありません。
- グローバルが汚染されていない
RequireJSを使うと、defineでくくり、公開する内容を自分で指定できるので、自然とグローバル領域は自然と汚染されない書き方になります。
(グローバルへの定義はRequireJSの提供するrequire関数、define関数のみとなっています)
- constantがプライベートに
addon.jsでのconstantへの直接アクセスもエラーとなり、公開されたsetConstantメソッドで書き換えるしかなくなりました。
これにより、site-common.js設計者の意図が守られます。
- レンダリング速度の向上
RequireJS使用前と使用後のネットワーク状態を比較してみます。
使用前
使用後
スクリプトのローディングを非同期で行うことにより、DOMの描画が並行で行われています。
結果的にDOMContentLoadedのタイミングはかなり早くなりました。
スクリプトの読み込みが終わらなくても、ユーザの目にサイトが表示されるので、待ち時間がより少ないと感じるはずです。
これらのメリットは、開発規模が大きければ大きいほど、開発人数が増えれば増えるほど、大きな効果をもたらすと思います。
さらに
RequireJSには、最適化ツール(r.js)を用いて、各モジュールをminifyし結合することができます。
これにより、さらなるパフォーマンス向上も見込めます。
そこについては、またその内。。。
公式サンプルから学ぶAngularJS (2)
ということで、前回の記事の続きで、AngularJS公式サイトのサンプル3つ目を作っていきます。
の前に、今回のサンプルはFirebaseというサービスを利用しているので、事前に登録を済ませておく必要があります。
さくっとできるのでぜひ試してみてください→以前にFirebaseに登録してみた記事
ではさっそく
サンプルその3「Wire up a Backend」
ブックマークの登録・編集・削除ができるサンプルです。
Firebaseと連携し、リアルタイムに他ユーザの変更も反映するようになっています。
イメージが沸きやすい様に、先に動作サンプル
今回のサンプルでやっていることは、大きく次の3つ
- ngRouteモジュールを使った、URLによるページ内表示内容の切り分け
- AngularFireを使ったFirebaseサービスとの連携
- 動的なフォームバリデーション
また、ソースの構成は以下
- index.html (メインHTML)
- project.js (メインJS)
- list.html (テンプレートとして読み込む)
- detail.html(テンプレートとして読み込む)
ここから、各ソースについて、自分なりの解説をしていきます。
※公式ページのサンプルソースとは、若干相違点もあります。
ソース
index.html
[a] AngularJS本体に加えて、ngRouteモジュールを読み込んでいます。ngRouteモジュールは、ng-view属性を指定した要素([d]の部分)に表示する内容をURL毎に設定することができる機能です。
[b] Firebaseサービスを使うためにAngularJS用Firebaseモジュールを読み込みます。
[c] さらに、見た目を少しよくしたいので、bootstrap.cssを読み込んでいます。(bootstrapはcssフレームワークです。簡単かつ迅速にサイトレイアウトを実現することが可能になります。ここでは詳細は省略します)
project.js
[a] まず、html全体に対するモジュール(project)を定義する際に、依存モジュールとしてngRouteおよびfirebaseを追加しています。これにより、このモジュール内にて、依存モジュールで新たに提供される要素にアクセスすることが可能になります。
[b] 'fbURL'という名前で、FirebaseサービスのベースURLを登録しています。
※value()は定数の定義ではなく、オブジェクトインスタンスの登録なので、文字列だけではなく数値、オブジェクト、関数等も登録できます。
[c] ここでは'Projects'という名前でFirebaseを利用するためのインスタンスを取得する関数を登録しています。
同じURLのサービスインスタンスを何度も作成する場合には、このようにfactory()を用いて登録しておきます。
[d] config()はモジュールの設定を行う関数です。ここではngModuleの設定を行っています。
URLに対して、対応するコントローラとテンプレート用HTMLを指定しています。パラメータとして値を渡したい場合には'/edit/:projectId'の用に記述します(20行目)
[e] 各コントローラの挙動を定義しています。先程パラメータとして渡した変数は、$routeParamからアクセスが可能です。(48,49行目)
list.html
[a] aタグのhrefには、最初に#をつけてページ外に飛ばないようにします。ngRouteモジュールを使って、ページ内の表示切替をする場合のテクニックです。
[b] ng-repeatには、filterやsearchを指定することで、表示する内容を制御することができます。ちなみに、orderByPriorityはFirebaseが返すオブジェクトを配列に変換するため、AngularFireが提供している機能です。Firebaseから取得したデータをng-repeatする際のおまじないだと考えて間違いないかと。
detail.html
[a1-a5] ここではフォームのバリデーションをしています。各フォームの部品に対して、以下のような属性を適宜追加することで、ユーザに優しい動的なフォームバリデーションが可能となります。
- ng-class="{クラス名:条件}"で、指定した条件が満たされたときに、この要素に追加されるクラス名を指定
- ng-show="条件" 条件が満たされたときに表示(満たされていないときには非表示になる)
- ng-disabled="条件" 条件が満たされたときに無効
また上記に設定する条件は、基本的なものは既に提供されています→バリデーションに関する説明とか(英語)
今回使用しているものは以下
- .$invalidで、入力値が無効かどうか(フォームの場合は、フォーム内の項目で1つでも無効なものがあれば無効判定)
- .$pristineで、未だユーザが値を入力したことがない(一度でも何かを入力したらその後はずっとfalse)
- .$error.xxxxで、xxxxに指定された形式でのフォーマットチェックを行います。urlでurl形式かどうか、invalidは必須判定
[b] このdetail.htmlテンプレートは、ブックマークの新規作成時と編集時に呼び出されますが、$scope.projectにFirebaseサービスインスタンスが設定されているのは編集時のみです。なので、project.$removeで削除ボタンの表示・非表示の制御を行っています。
大筋としては、こんな感じだと思います。解釈がおかしい点も多々あると思うので、鵜呑みにはしないでください。
ソースの説明が飛び飛びで分かりづらいと思うので、Firebaseとの連携、ngRouteモジュールの使用についてだけ、ざっくり以下に纏めました。
Firebaseとの連携部分について超ざっくり説明
1.firebase.js、angularfire.jsを読込
<script src="https://cdn.firebase.com/v0/firebase.js"></script> <script src="https://cdn.firebase.com/libs/angularfire/0.7.1/angularfire.min.js"></script>
2.依存モジュールとして、'firebase'を追加
var mainModule = angular.module('project', ['firebase']);
3.取得したいデータに対するサービスインスタンスを取得
var instance = $firebase(new Firebase('https://intense-fire-xxxx.firebaseio.com/angularSample3/[キー]/[キー]/...'));
上記でFirebase内のid(複数指定でその階層までさかのぼる)に対応したデータに対応するオブジェクトを取得できる
4.取得したサービスインスタンスを使って、データの取得や変更を行う。
主なプロパティとメソッドとしては
- instance.[キー]でその内部データにアクセス
- instance.$add(オブジェクト, コールバック関数):その階層に新たにオブジェクトを追加
- instance.$save():DOMの内容とFirebase内の情報を同期
- instance.$remove()、対象のデータをFirebaseから削除
ngRouteモジュールの使用部分について超ざっくり説明
1.angular-route.jsを読込
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular-route.min.js"></script>
2.表示を切り替えたい要素に対して、ng-view属性を追加
<div ng-view></div>
3.依存モジュールとして、'ngRoute'を追加
var mainModule = angular.module('project', ['ngRoute']);
4.angular.module.config()で、ngRouteモジュールの設定(=urlとコントローラ、テンプレートの紐付け)を行う
※各コントローラの挙動の定義も行う必要があります。
mainModule.config(function($routeProvider) { $routeProvider .when('/', { controller:'ListCtrl', templateUrl:'list.html' }) .when('/edit/:projectId', { controller:'EditCtrl', templateUrl:'detail.html' }) .when('/new', { controller:'CreateCtrl', templateUrl:'detail.html' }) .otherwise({ redirectTo:'/' }); })
5.aタグでのページ遷移時には、#を最初に指定する
<a href="#/" class="btn">Cancel</a>
やってみて...
書き始めて10分後ぐらいに思ったことですが、1つのサンプルで3つのことを盛り込むよりも、個別にもっとシンプルなソースで、1つずつ説明したほうがきっといいんだろうなと思いました。。。
公式サンプルは、チュートリアルではなくて、こんなことができますよ程度に捉えたほうがいいのかも(いまさら)
次の公式サンプル4は、いずれまた。。。
リアルタイムのバックエンドサービスFirebaseに感動
Firebaseが「何これやべぇ」ってお話。
対話的リアルタイムアプリケーションのためのバックエンドサービスなんですが、
とりあえず、アカウント登録して、サンプルを動かすだけで、そのリアルタイム感に感動できます。
Firebase - Build Realtime Apps
アカウント登録の手順
1. 右上の「SIGN UP FREE」を押す
2. 登録するメルアドとパスワードを入力して、「Create My Account」
これで登録自体は完了です。
この後は、チュートリアル的な感じです。
3.登録後の画面で、「View Firebase」を押すと、ストレージの中がGUIで見れる
4. 赤い矢印の辺りがデータをツリー構造で表示してるGUIで、「新しいデータ登録してみ」とか「サンプルデータ入れてみ」とか聴かれるので、そのとおりにやってるといきなりポップアップでチャットウインドウが表示される。
5. このチャットアプリを複数ウインドウで立ち上げて、エア友達とチャットしている気になると楽しいw
データも更新される度に、リアルタイムでストレージのGUI表示も更新されるので、これを観ながらチャットすると尚楽しい。
このサンプルチャットアプリも5分で作れるというから驚き。(手順は以下)
Tutorial | Firebase
「何これやべぇ」
2014/5/8 追記
このFirebaseを使ったAngularJS公式サンプルを作りながら自分なりに解説してみた記事はこちら↓
公式サンプルから学ぶAngularJS (2) - 1010realのブログ ver2
JSFiddleでAngularJSを使う
前回の記事でのAngularJSのサンプルはJSFiddle内で動作確認してみたのですが、AngularJSをJSFiddle内で使うのに、ちょっとだけテクニックが必要だったのでまとめてみました。
1. まず、AngularJSを使いたいfiddleのFrameworks & Extensions設定を以下の設定にします。
- No-Library(pure JS)
- No wrap - in
2. css内に以下のコードを貼り付けます。
3. html内ではbodyタグは使えないので、bodyにng-appを追加している場合はdivに置き換える。
これで、AngularJSが利用できます。
Frameworks & Extensions設定の中でAngularJSの項目もありますが、選べるバージョンが少ないのと、設定によってうまく動かなかったりしたのでこちらの方法で動作確認しました。
この方法だと、自分が利用したいバージョンでAngularJSが読み込めます。
しかし、JSFiddle便利。。。
公式サンプルから学ぶAngularJS (1)
大層なタイトルをつけちゃいましたが、やってることは以下。
AngularJSの公式サイト(以下)のトップに乗っているサンプルを作りながら、AngularJSのチュートリアル的なことをしてみる。
英語:AngularJS — Superheroic JavaScript MVW Framework
日本語訳:AngularJS入門 | AngularJS 1.2 日本語リファレンス | js STUDIO
AngularJSとは?
間違ったことを書いてもアレなので、大部分を抜粋させていただきました。
- AngularJSは動的Webアプリケーション構築のためのフレームワーク
- データバインディングと依存注入を通して大部分の余計なコードを排除できる。分かりやすく言うと、動的アプリケーションを作るための様々な苦労(=JavaScriptでの大量なコード記述)を請け負ってくれるので、素早く機能開発を開始できる
- CRUDアプリケーションとかに特に向いている。逆に少し複雑な事(AngularJSのルールから外れること)をしようとするととたんにコストがあがるため、ゲーム等のバリバリのアニメーションとかには向かない
では早速サンプルを作ってきます。
サンプルその1「The Basics」
フォームに入力した値を、リアルタイムにDOM要素へ反映するサンプルです。
ソース
angular_1.html
- a. まずは、AngularJSを読み込みます
- b. AngularJSを適用する要素に、ng-app属性を追加します。(今回は文書全体に適用)
- c. テキストボックスに対し、ng-model="変数名"な属性を追加します。
- d. {{変数名}}でモデル内の変数を出力します。
動作イメージ
テキストボックスの値を変更すると、同時に出力されているDOM要素が更新されます。
上記の指定だけで、AngularJSが
「フォームの部品(=UI)と表示データ(=モデル)の紐付けと、どちらかが変更された際の同期処理」
をしてくれます。
これは「双方向データバインディング」と呼ばれ、AngularJSの大きな特徴の1つです。
また、AngularJSはブラウザに様々な記法を提供します(ngApp、ngModel等)
これを提供する仕組みを「ディレクティブ」と呼びます。
この仕組みを使って、自分で新たに記法を定義することもできます。
※ 他のモジュールを追加で読み込むことで使用できるようになる記法もあります
→ AngularJSディレクティブ一覧 (ngRouteより下)
サンプルその2「Add Some Control」
TODO管理を通して、要素の追加、読込、更新、削除を試すサンプルです。(バックエンドとの連携はなし)
ソース
angular_2.html
- a. todo.jsを読み込みます。todo.jsには、コントローラ(TodoCtrl関数)が定義されています。
- b. コントローラにより制御を行う要素に、ng-controller属性を追加します。
- c1-4. コントローラ内で$scopeに定義したプロパティやメソッドを参照して、各挙動を定義していきます。
todo.js
- a1-4. $scopeにプロパティやメソッドを定義します。これらは、ng-controller属性を追加した要素内から参照できます。
todo.css
動作イメージ
各UIからtodoの管理が行えます。
ここで大切なのは、todo.js内では、「データ(=モデル)をどうするか」しか定義していません。
「変更したデータをどう表示するか」については、AngularJSが全て請け負ってくれました。
という訳で、あっという間に要素の追加、読込、更新、削除を実現できてしまいました。
AngularJSが得意な分野の動的アプリケーションを作れば、相当な開発効率アップが見込める事が体感できたかと思います。
AngularJSの公式サイトにはあと2つのサンプルが紹介されています。
それらのサンプルについては、また今度。。。
2014/5/8 追記
公式サンプル3についても、作ってみました↓
公式サンプルから学ぶAngularJS (2) - 1010realのブログ ver2
jQuery.Deferredをちゃんと理解する
ちゃんと理解するために、ちゃんと説明してみた。
jQuery.Deferredは、タスクを管理する仕組み
jQuery.Deferredでできることは、以下のような事です。
- お仕事の約束をする
- お仕事が終わったら結果を受け取る
- 受け取った結果を元に次の作業をする
これって、タスク管理ですね。
自分としてはこの書き方が一番理解しやすい気がします。
実は、jQuery.ajax()も内部でDeferredオブジェクトを使っています。
でも、古いjQueryをサポートするために、Deferredオブジェクトを使わない書き方も出来ちゃいます。
Deferredオブジェクトを使わない場合と、使う場合で書いてみます。
Deferredオブジェクトを使わない
従来の、コールバックに次の処理を書いてく感じです。
読み込むURLが増えれば増えるだけ死にたくなります。
Deferredオブジェクトを使う
jQuery.ajax()の戻り値はpromise Object(deferred.promise()の戻り値)になってます。
これを使うと、メソッドチェーンを使ってこんな感じで書けます。
このdone(),fail()関数は、Deferredオブジェクトのメソッドです。
(詳しくはDeferred Object | jQuery API Documentation)
- deferred.done():タスクが成功したときに実行する関数を指定
- deferred.fail():タスクが失敗したときに実行する関数を指定
実際の使い方
ではここから、実際にDeferredオブジェクトを使って、タスク管理の仕組みを作っていきます。
タスクの処理側を作ってみる
jQuery.ajax()の内部でしている事を理解するため、自分で作った関数に置き換えてみます。
func1は、2秒後に0,1の乱数判定をして、1で成功(resolve)、0で失敗(reject)を返す関数です。
実行すると大体50%の確率で、成功なら「success!」、失敗なら「failed...」と言われますね。
ここで、10-12行を観ると、$.ajax(...)をfunc1()に置き換えた形になってます。
つまり、jQuery.ajax(...)の中でも、func1()と同じようなことをしてます。
これがタスクの処理側の基本的なしくみです。
- deferred.resolve():タスクが正常に終わったことを通知(引数に指定した内容が、done()に引き渡される)
- deferred.reject():タスクが異常終了した事を通知(引数に指定した内容が、fail()に引き渡される)
- deferred.promise():タスク(タスクをこなしますという約束)
Deferredオブジェクトの便利さを実感する
次にこのタスクを3回連続で行ってみます。
コールバックだとネストinネストinネストになるので、ちょっとカオスるやつですね。
タスク終了時に、再度タスクを実行して、そのpromise Objectをreturnしてあげれば、
メソッドチェーンでどんどん次の処理を記述できるので、どんどん下に追記して行けますね。
3つどころか、いくつでも来なさいって感じ。どんどんやっちゃってー
- then():タスクが終了した時に、結果がresolveなら第1引数の関数、rejectなら第2引数の関数を実行
Deferredオブジェクトの便利さを痛感する
次にこの3つのタスクを並列で行ってみます。
コールバックだと、3つとも終わったタイミングを検知するためにちょっと試行錯誤が必要ですね。
jQuery.when()は、渡されたタスクを並列で実行し、全てが成功したタイミングでdone()、1つでも失敗したタイミングでfail()を実行してくれます。
これを使う為にjQuery.Deferredを作ったんじゃねーかと思ってます。
ちなみにfailに渡される引数は1つで、reject対象の引数のみが渡されます。
以下のソースで確認できます。
渡された変数を全て出力してみると、成功時は3回、失敗時1回だけになりますね。
実用的な何かを作ってみる
Deferredオブジェクトを使って、画像のプリロードをしてみました。
読み込む画像の数が何個増えたって怖いものなしです。
ajax処理と並行する事もできます。Deferred万歳!