albatrosary's blog

UI/UXとエンタープライズシステム

初心者向けAngularJS - その2

続きましてもうちょっとかっこよくしていきたいと思います。かっこいいというかJavaScriptファイルが一つですので数人で開発するときにはちょっと困りますね。なので少し「かっこよく」です。前回はビルトインディレクティブと簡単なコントローラーとファクトリーまで行いました。

初心者向けAngularJS - その1 - albatrosary's blog

ゴールですが

  • ファイルを用途に合わせて分割する
  • ルーターを導入しページ遷移を作成する

です。前回作ったものを流用する形ですが、作業量が少し多いですので焦らずにご自身のペースで進められてはと思います。分かりづらい事項はコメントを入れていただくかツイートして頂けたらと思います。 ファイル分割は最終的に次のようになります:

f:id:albatrosary:20141106200927p:plain

少し難し話

コーティングを始める前に少し背景を。
最近ではWebアプリケーションを「SPA(Single-page Application)」で作成することが多くなっています。なぜSPAなのかというと色々ありますが、そのひとつ、Appleショックにより禁じ手となったFlex、Silverlight、JavaアプレットなどのプラグインベースRIA製品の代替として、また、JavaScriptを含むHTML5の高度な機能を大規模開発でも利用していこうということがあげられます。シングルページでないもは「Multi-page」と言いますが、これについては後付けで名前がついたのではと思います。特徴のひとつになりますが、ほとんどの処理をブラウザで行っていますのでJavaScriptでがっつりコーティングする必要がでてきます。

Rich Internet Application(RIA) から Single-page Application(SPA) へ - albatrosary's blog

SPAを実現するための基本的な機能には

  • ルーティング
  • テンプレート
  • コントローラー
  • 通信

があります。コンテンツ部分を表現するHTMLをテンプレートと読んでいます。コントローラーはJavaScriptを書く場所です。ルーティングというのはコンテンツ部分を切り替えるための仕組みで「この url が指定されたら、このテンプレートを表示してこのコントローラーを実行しよう!」ということです。
さすがにブラウザで入力したデータを抱え込んでしまっては業務になりませんので、サーバと通信をします。ここで利用されるのがRESTという方法です。

SPAを開発するために多くのJavaScriptフレームワークがあり両手でも数え切れないくらいフレームワークやライブラリが存在します。代表的なものとしては

  • Backbone.js
  • AngularJS
  • Ember.js
  • KnockoutJS
  • React.js

そのうちの一つが今回テーマとしてあげているAngularJSです。AngularJSでは

  • ルーティング:ui-route
  • 通信:$http もしくは $resource
  • コントローラー:controller (および service, factory)
  • テンプレート:htmlファイル

先ほど説明した通り url が指定されたら、どのテンプレートを使ってどのコントローラを利用するかというのを定義しているのが $stateProvider です。このブログではこれを「状態」と言っています。

今回は、ルーティング/コントローラー/テンプレートまで実装します。

ファイルを分割する

意味あるグループでディレクトリを作りファイル分割しましょう。

プロジェクトフォルダー
|- index.html
|- header.html
|- app
   |- app.js
   |- controller
   |   |- main
   |   |   |- mainCtrl.js
   |   |- footer
   |       |- footerCtrl.js
   |- factory
       |- factory.js

index.html

index.html は分割した JavaScriptファイルをすべて読み込ませる必要がありますので定義します。

<!doctype html>
<html class="no-js">
  <head>
    <meta charset="utf-8">
    <title>AngularJSの勉強</title>
    <meta name="description" content="">
    <meta name="viewport" content="width=device-width">
  </head>
  <body ng-app="app">
    <div ng-include="'header.html'"></div>
    <div ng-controller="mainCtrl">
      <!-- 何かを記載 -->
      <input type="button" value="クリックでメッセージ表示" ng-click="onClick()">
      <span ng-bind="::message"></span>
    </div>
    <div ng-controller="footerCtrl">
      <!-- 何かを記載 -->
      <input type="button" value="クリックでメッセージ表示" ng-click="onClick()">
      <span ng-bind="::message"></span>
    </div>
    <script src="bower_components/angular/angular.js"></script>
    <!-- アプリケーションファイル -->
    <script src="app/app.js"></script>
    <script src="app/factory/factory.js"></script>
    <script src="app/controller/main/mainCtrl.js"></script>
    <script src="app/controller/footer/footerCtrl.js"></script>
  </body>
</html>

app.js

app.jsファイルはメインとなる JavaScriptファイルですが内容は(いまは)ほとんど記載することがありません。

'use strict';

angular.module('app', []);

急に「'use strict';」という文字が出てきました。これは「strictモード(厳格モード)」と呼ばれています。strict モードでは、通常の JavaScript の意味にいくつかの変更を加えます。第一に strict モードでは、JavaScript でエラーではないが落とし穴になる一部の事柄を、エラーが発生するように変更することで除去します。第二に strict モードでは、JavaScript エンジンによる最適化処理を困難にする誤りを修正します。

Strict モード - JavaScript | MDN

factory.js

さて次に factory.js を見ていきます。

'use strict';

(function (){
var Factory = function () {
    // 共通処理
    var DEFUALT_MESSAGE = "AngularJS:";
    return {
      showMassage: function (message) {
        return DEFUALT_MESSAGE + message;
      }
    }
  };
  
angular.module('app')
  .factory('factory', Factory);    
})();

ですが、次のように少し書き方を変えましょう。

'use strict';

angular.module('app')
  .factory('factory', function () {
    // 共通処理
    var DEFUALT_MESSAGE = "AngularJS:";
    return {
      showMassage: function (message) {
        return DEFUALT_MESSAGE + message;
      }
    }
  });

ここで見てもらいたいのが app.js では angular.module の書き方が

angular.module('app', [])

でしたが、今回は

angular.module('app')

です。前回学んだ「定義」と「モジュール呼び出し」の違いを思い出してください。

mainCtrl.js

さて mainCtrl.js は

'use strict';

angular.module('app')
  .controller('mainCtrl', function ($scope, factory) {
    // controllerの中身
    $scope.onClick = function () {
      $scope.message = factory.showMassage("AngularJSアプリケーション");
    };
  });

footerCtrl.js

footerCtrl.js は

'use strict';

angular.module('app')
  .controller('footerCtrl', function ($scope, factory) {
    // controllerの中身
    $scope.onClick = function () {
      $scope.message = factory.showMassage("ここはフッター");
    };
  });

です。コントローラーではそれぞれ factory という引数がありますが、これがファクトリー定義したfactory を呼び出しているという意味です。これを「インジェクション」と呼びます。

ここまでの成果

単純にファイル分割したという内容はこれで終了です。いままでと同じように無事アプリケーションが動くはずです(余談ですが main と footer と名前付けしたのはちょっとセンスが疑われますね)。

ルーティングを使ってみる

ui-routeのインストール

ルーティングルールを定義します。ルールとしては

localhost:8000/ のとき mainCtrl を処理
localhost:8000/#/footer のとき footerCtrl を処理 

まずルーティングするために ui-route をインストールしましょう。

UI-Router

bowerを使っている場合は

bower install angular-ui-router 

でインストールできます。HTMLファイルにui-routeの定義を追加します。

<!doctype html>
<html class="no-js">
  <head>
    <meta charset="utf-8">
    <title>AngularJSの勉強</title>
    <meta name="description" content="">
    <meta name="viewport" content="width=device-width">
  </head>
  <body ng-app="app">
    <div ng-include="'header.html'"></div>
    <!-- 機能
    〜 省略 〜  
    -->
    <script src="bower_components/angular/angular.js"></script>
    <script src="bower_components/angular-ui-router/release/angular-ui-router.js"></script>
    <!-- アプリケーションファイル -->
    <script src="app/app.js"></script>
    <script src="app/factory/factory.js"></script>
    <script src="app/controller/main/mainCtrl.js"></script>
    <script src="app/controller/footer/footerCtrl.js"></script>
  </body>
</html>

ダウンロードしたときには angular.js と同じ階層に配置したとすると

<!doctype html>
<html class="no-js">
  <head>
    <meta charset="utf-8">
    <title>AngularJSの勉強</title>
    <meta name="description" content="">
    <meta name="viewport" content="width=device-width">
  </head>
  <body ng-app="app">
    <div ng-include="'header.html'"></div>
    <!-- 機能
    〜 省略 〜  
    -->
    <script src="angular.js"></script>
    <script src="angular-ui-router.js"></script>
    <!-- アプリケーションファイル -->
    <script src="app/app.js"></script>
    <script src="app/factory/factory.js"></script>
    <script src="app/controller/main/mainCtrl.js"></script>
    <script src="app/controller/footer/footerCtrl.js"></script>
  </body>
</html>

ここままだと angular-ui-router.js ファイルを読み込んだだけでアプリケーションとしてはまだ利用できません。

ui-router の利用宣言

angular で利用するためには宣言する必要があります。 app.js を開き「宣言」をします。

'use strict';

angular.module('app', ['ui.router']);

重要な一文です。の中に'ui.router'と記載しました。ここでようやくの意味が理解できたのではないかと思います。

指定された url ごとに「機能」を入れ替える処理を定義

表示させたい内容を定義したHTMLファイルを作成します。

main.html

mainCtrl に対する main.html は

<div ng-controller="mainCtrl">
  <!-- 何かを記載 -->
  <input type="button" value="クリックでメッセージ表示" ng-click="onClick()">
  <span ng-bind="::message"></span>
</div>

footer.html

footerCtrl に対する footer.html は

<div ng-controller="footerCtrl">
  <!-- 何かを記載 -->
  <input type="button" value="クリックでメッセージ表示" ng-click="onClick()">
  <span ng-bind="::message"></span>
</div>

index.html

main.html と footer.html を外だしにしましたので index.html は

<!doctype html>
<html class="no-js">
  <head>
    <meta charset="utf-8">
    <title>AngularJSの勉強</title>
    <meta name="description" content="">
    <meta name="viewport" content="width=device-width">
  </head>
  <body ng-app="app">
    <div ng-include="'header.html'"></div>
    <script src="angular.js"></script>
    <script src="angular-ui-router.js"></script>
    <!-- アプリケーションファイル -->
    <script src="app/app.js"></script>
    <script src="app/factory/factory.js"></script>
    <script src="app/controller/main/mainCtrl.js"></script>
    <script src="app/controller/footer/footerCtrl.js"></script>
  </body>
</html>

となっているはずです。如何でしょうか?道具は揃いました。

ここまでの成果

JavaScriptファイルについては、app.js、factory.js、mainCtrl.js、footerCtrl.jsとファイル分割しました。HTMLについては、index.htmlとmain.html、footer.htmlにファイル分割しました。ここまでの作業は意味のある単位でファイルを作成するという目的で行ってきましたが如何でしょうか?ちょっと整理できるまで時間が必要になるかもしれません。でも、残念ながらファイル分割の作業の最後が次になります。

$stateProvider で状態を定義

ひとつ足りないものがあります。ある url が指定されたときに、どのモジュールが実行されるかまだ定義していません。定義ファイルとして、 mainCtrl用には main.js、footerCtrl用には footer.js を用意します(またJavaScriptファイルが増えましたね...)。

main.js

main.js は

'use strict';

angular.module('app')
  .config(function ($stateProvider) {
    $stateProvider
      .state('main', {
        url: '/',
        templateUrl: 'app/controller/main/main.html',
        controller: 'mainCtrl'
      });
  });

これは「状態」を main と定義しています。状態「main」は次の特徴があります。

  • 呼び出す url を /
  • 表示する HTMLは main.html
  • 利用するコントローラーは mainCtrl

footer.js

footer.jsは

'use strict';

angular.module('app')
  .config(function ($stateProvider) {
    $stateProvider
      .state('footer', {
        url: '/footer',
        templateUrl: 'app/controller/footer/footer.html',
        controller: 'footerCtrl'
      });
  });

となります。こちらは

  • 呼び出す url を /footer
  • 表示する HTMLは footer.html
  • 利用するコントローラーは footerCtrl

として定義しています。「$stateProvider」という単語が出てきましたが、このように$stateProviderは「状態」を定義しています。

ここまでの整理

ディレクトリをもう一度整理しますので確認してください。

プロジェクトフォルダー
|- index.html
|- header.html
|- app
   |- app.js
   |- controller
   |   |- main
   |   |   |- main.html
   |   |   |- main.js
   |   |   |- mainCtrl.js
   |   |- footer
   |       |- footer.html
   |       |- footer.js
   |       |- footerCtrl.js
   |- factory
       |- factory.js

多くのJavaScriptファイルを作成しましたので index.html でもきちんと読み込ませているか確認しましょう。

<!doctype html>
<html class="no-js">
  <head>
    <meta charset="utf-8">
    <title>AngularJSの勉強</title>
    <meta name="description" content="">
    <meta name="viewport" content="width=device-width">
  </head>
  <body ng-app="app">
    <div ng-include="'header.html'"></div>
    <script src="bower_components/angular/angular.js"></script>
    <script src="bower_components/angular-ui-router/release/angular-ui-router.js"></script>
    <!-- アプリケーションファイル -->
    <script src="app/app.js"></script>
    <script src="app/factory/factory.js"></script>
    <script src="app/controller/main/mainCtrl.js"></script>
    <script src="app/controller/main/main.js"></script>
    <script src="app/controller/footer/footerCtrl.js"></script>
    <script src="app/controller/footer/footer.js"></script>
  </body>
</html>

ui-view

最後に、外だしにしたHTMLを読み込ませる場所を定義する必要があります。

    <div ui-view=""></div>

というディレクティブを使うとその場所に個別定義した HTML(main.htmlやfooter.html) が差し込まれます!

<!doctype html>
<html class="no-js">
  <head>
    <meta charset="utf-8">
    <title>AngularJSの勉強</title>
    <meta name="description" content="">
    <meta name="viewport" content="width=device-width">
  </head>
  <body ng-app="app">
    <div ng-include="'header.html'"></div>
    <div ui-view=""></div>
    <script src="bower_components/angular/angular.js"></script>
    <script src="bower_components/angular-ui-router/release/angular-ui-router.js"></script>
    <!-- アプリケーションファイル -->
    <script src="app/app.js"></script>
    <script src="app/factory/factory.js"></script>
    <script src="app/controller/main/mainCtrl.js"></script>
    <script src="app/controller/main/main.js"></script>
    <script src="app/controller/footer/footerCtrl.js"></script>
    <script src="app/controller/footer/footer.js"></script>
  </body>
</html>

手間暇掛けてここまでファイル分割とルーターの定義をしました。かなりの苦労だったのではないかと思います。実際に動かしてみましょう。

http://localhost:8000/

のとき

http://localhost:8000/#/footer

のとき動きを確認してください。どうですか!
これでようやくルーターを使えるようになりました。なかなかボリュームがあったと思います。

$urlRouterProvider の利用

ただこの場合だと適当な url が指定された場合、たとえば

http://localhost:8000/#/hoge

のようなurlが指定された場合、ボタンが表示されないことを確認してください。適当な url が指定された場合には

http://localhost:8000/

と同じ main.html が表示されるようにしましょう。app.js を開いてください。

'use strict';

angular.module('app', ['ui.router'])
  .config(function ($urlRouterProvider) {
    $urlRouterProvider
      .otherwise('/');
  });

$urlRouterProvider.otherwise('/')を指定することで、定義されてない url がきたら '/' (つまり main)に遷移させますという宣言をしています。

ui-sref(簡単なメニューを作る)

せっかくルーティング作りましたのでメニュー風な要素をいれましょう。header.htmlを開き url を定義します。

<a ui-sref="state">タイトル</a>

を追加します。state部分は main.js、footer.jsで定義したときの .state 横に記載したものです。それに()を付け加えています。

angular.module('app')
  .config(function ($stateProvider) {
    $stateProvider
      .state('main', {

header.html は次のようになります。

<h1>AngularJS勉強会</h1>
<a ui-sref="main()">メイン</a>
<a ui-sref="footer()">フッター</a>

ルーティングについてはもっといろいろ機能がありますが、ここまでとします。大きなシステムを構築する場合、これだけファイル分割するとかなり見にくくなります。そのために「ビルド」しファイルを繋げ圧縮します。本番環境へリリースするときはかなり複雑なプロセスをしますので grunt や gulp で処理を定型化しておきます。多くのファイルを作ったりui-routeのように必要な機能が欲しいときに追加など様々なことを開発ではします。そのために YEOMAN を使って開発プロセスを定型化します。

出来上がり

出来上がりは次のようになります。尚ソースは github にありますので合わせて見ていただけたらと思います。

https://github.com/albatrosary/angular-seminar/tree/master

f:id:albatrosary:20141106203101p:plain

f:id:albatrosary:20141106203114p:plain

f:id:albatrosary:20141106203126p:plain

f:id:albatrosary:20141106203138p:plain