albatrosary's blog

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

generator-webapp をベースに AngularJS を利用していくのが学習としては良いということなのでやってみた

先日のYEOMANハンズオンでgenerator-webappをベースにしてAngularJSのモジュールを入れていくのが学習効果としては高いというコメントを見たので実際にやってみました。目標としてはAngularJSとui-routerが使えれば良いかなということで次の通りとした。

  • angularのインストール
  • ui-routerのインストール
  • ルーティングの実装
  • ビルドの実施

前準備

前準備として generator-webapp を登録します:

$ npm install -g generator-webapp

次にプロジェクトを作成します。今回はオプションをすべて利用してみます:

$ mkdir webapp && cd $_
$ yo webapp

     _-----_
    |       |    .--------------------------.
    |--(o)--|    |    Welcome to Yeoman,    |
   `---------´   |   ladies and gentlemen!  |
    ( _´U`_ )    '--------------------------'
    /___A___\    
     |  ~  |     
   __'.___.'__   
 ´   `  |° ´ Y ` 

Out of the box I include HTML5 Boilerplate, jQuery, and a Gruntfile.js to build your app.
? What more would you like? 
 ◉ Bootstrap
 ◉ Sass
❯◉ Modernizr

サーバを起動して動作確認します:

$ grunt serve

f:id:albatrosary:20150106193848p:plain

AngularJSのbowerインストール

bowerサーチのサイトに行くとangularと名の付くモジュールが多く存在しますが今回は「angular」を利用します

Bower - Search

bowerインストールでAngularJSの登録

$ bower install angular --save
bower angular#*                 cached git://github.com/angular/bower-angular.git#1.3.8
bower angular#*               validate 1.3.8 against git://github.com/angular/bower-angular.git#*
bower angular#~1.3.8           install angular#1.3.8

angular#1.3.8 bower_components/angular
$ 

bower.jsonにも追加されているのが確認できます:

{
  "name": "webapp",
  "private": true,
  "dependencies": {
    "bootstrap-sass-official": "~3.2.0",
    "modernizr": "~2.8.2",
    "angular": "~1.3.8"
  }
}

angularの実行確認

HTMLのbodyタグに「ng-app」を追加、「{{1+1}}」を記載し「2」になれば問題無しです:

f:id:albatrosary:20150106195049p:plain

HTMLは次の通りです(部分的に書き出してます):

  <body ng-app>
    {{1+1}}

画面の構成

webappには

  • ヘッダー部
  • コンテンツ部
  • フッター部

があるのでコンテンツ部のみui-viewで切り替えるようにします。ヘッダーとフッターはng-includeを利用します:

f:id:albatrosary:20150106195936p:plain

またページは

  • home
  • about
  • contact

がありますので3ページ作ることとします。

ui-routerの登録

まず ui-router を登録します:

$ bower install ui-router --save
bower ui-router#*               cached git://github.com/angular-ui/ui-router.git#0.2.13
bower ui-router#*             validate 0.2.13 against git://github.com/angular-ui/ui-router.git#*
bower ui-router#~0.2.13        install ui-router#0.2.13

ui-router#0.2.13 bower_components/ui-router
└── angular#1.3.8
$ 

先ほど同様にbower.jsonは次の通りです:

{
  "name": "webapp",
  "private": true,
  "dependencies": {
    "bootstrap-sass-official": "~3.2.0",
    "modernizr": "~2.8.2",
    "angular": "~1.3.8",
    "ui-router": "~0.2.13"
  }
}

ファイル分割

ヘッダー、フッターとコンテンツにファイルを分割します。index.htmlは

    <div class="container">
      
      <div ng-include="'components/header/header.html'"></div>

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

      <div ng-include="'components/footer/footer.html'"></div>
    </div>

header.htmlとfooter.htmlはappディレクトリの直下にcomponentsディレクトリを作成し配置していきます。header.htmlは次の通り:

<div class="header">
  <ul class="nav nav-pills pull-right">
    <li class="active"><a href="#">Home</a></li>
    <li><a href="#">About</a></li>
    <li><a href="#">Contact</a></li>
  </ul>
  <h3 class="text-muted">webapp</h3>
</div>

footer.htmlは次の通り:

<div class="footer">
  <p><span class="glyphicon glyphicon-heart"></span> from the Yeoman team</p>
</div>

main.jsの変更

main.js をangularモジュールの読み込み元とするため次のように変更します。ここでui-routerの宣言も行います:

'use strict';

angular.module('webapp', [
    'ui.router'
  ])
  .config(['$locationProvider', '$urlRouterProvider',
  function ($locationProvider, $urlRouterProvider) {

    $urlRouterProvider
      .otherwise('/');

    $locationProvider.html5Mode(true);

  }
]);

homeの定義

home画面(最初に表示される画面)を定義するため

  • home.js
  • home.controller.js
  • home.html
  • home.scss

を定義します。コントローラー名は「HomeCtrl」としています。index.htmlにhome.jsとhome.controller.jsを読み込ませることを忘れずに。

        <!-- build:js({app,.tmp}) scripts/main.js -->
        <script src="scripts/main.js"></script>
        <script src="scripts/home/home.controller.js"></script>
        <script src="scripts/home/home.js"></script>
        <!-- endbuild -->

home.jsは:

'use strict';

angular.module('webapp')
  .config(['$stateProvider', 
  function ($stateProvider) {
    $stateProvider
      .state('home', {
        url: '/',
        templateUrl: 'scripts/home/home.html',
        controller: 'HomeCtrl'
      });
  }]);

home.controller.jsは:

'use strict';

angular.module('webapp')
  .controller('HomeCtrl', ['$scope', function ($scope) {
    
  }]);

home.htmlは:

<div class="jumbotron">
  <h1>'Allo, 'Allo!</h1>
  <p class="lead">Always a pleasure scaffolding your apps.</p>
  <p><a class="btn btn-lg btn-success" href="#">Splendid! <span class="glyphicon glyphicon-ok"></span></a></p>
</div>

<div class="row marketing">
  <div class="col-lg-6">
    <h4>HTML5 Boilerplate</h4>
    <p>HTML5 Boilerplate is a professional front-end template for building fast, robust, and adaptable web apps or sites.</p>

    <h4>Sass</h4>
    <p>Sass is a mature, stable, and powerful professional grade CSS extension language.</p>


    <h4>Bootstrap</h4>
    <p>Sleek, intuitive, and powerful mobile first front-end framework for faster and easier web development.</p>

    <h4>Modernizr</h4>
    <p>Modernizr is an open-source JavaScript library that helps you build the next generation of HTML5 and CSS3-powered websites.</p>

  </div>
</div>

ここまで実装し実行してもエラーになります。内容は

Error: [$location:nobase] $location in HTML5 mode requires a tag to be present!

ですがこれはheadタグの中に

<base href="/">

が含まれていないためで、登録します:

<!doctype html>
<html class="no-js">
  <head>
    <meta charset="utf-8">
    <base href="/">
    <title>webapp</title>
    <meta name="description" content="">
    <meta name="viewport" content="width=device-width">
    <link rel="shortcut icon" href="/favicon.ico">
   …以下省略 …

そうすると

f:id:albatrosary:20150106203740p:plain

jshintを修正

これだけでは実は終わって無くjshintを修正します。コンソールをみるとわかるのですが

app/scripts/home/home.js
  line 3  col 1   'angular' is not defined.

app/scripts/main.js
  line 3  col 1   'angular' is not defined.

というような警告が発生しています。「angular」はグローバルですのでjshintに教える必要があります。jshintは「.jshintrc」というファイルに定義されていますので次の定義を追加します:

  "globals": {
    "angular": true
  }

こうすると「.jshintrc」は

{
  "node": true,
  "browser": true,
  "esnext": true,
  "bitwise": true,
  "camelcase": true,
  "curly": true,
  "eqeqeq": true,
  "immed": true,
  "indent": 2,
  "latedef": true,
  "newcap": true,
  "noarg": true,
  "quotmark": "single",
  "undef": true,
  "unused": true,
  "strict": true,
  "trailing": true,
  "smarttabs": true,
  "jquery": true,
  "globals": {
    "angular": true
  }
}

これで実行すれば警告が消えます。

aboutの定義

about画面(最初に表示される画面)を定義するため

  • about.js
  • about.controller.js
  • about.html
  • about.scss

about.jsは

'use strict';

angular.module('webapp')
  .config(['$stateProvider', 
  function ($stateProvider) {
    $stateProvider
      .state('about', {
        url: '/about',
        templateUrl: 'scripts/about/about.html',
        controller: 'AboutCtrl'
      });
  }]);

about.controller.jsは

'use strict';

angular.module('webapp')
  .controller('AboutCtrl', ['$scope', function ($scope) {
    
  }]);

about.htmlは

about.html

contactの定義

contact画面(最初に表示される画面)を定義するため

  • contact.js
  • contact.controller.js
  • contact.html
  • contact.scss

about.jsは

'use strict';

angular.module('webapp')
  .config(['$stateProvider', 
  function ($stateProvider) {
    $stateProvider
      .state('contact', {
        url: '/contact',
        templateUrl: 'scripts/contact/contact.html',
        controller: 'ContactCtrl'
      });
  }]);

contact.controller.jsは

'use strict';

angular.module('webapp')
  .controller('ContactCtrl', ['$scope', function ($scope) {
    
  }]);

contact.htmlは

contact.html

メニュー作成

ui-routerの利用

ヘッダーのメニューでページが切り替わるように変更します。いまヘッダーは

<div class="header">
  <ul class="nav nav-pills pull-right">
    <li class="active"><a href="#">Home</a></li>
    <li><a href="#">About</a></li>
    <li><a href="#">Contact</a></li>
  </ul>
  <h3 class="text-muted">webapp</h3>
</div>

となっていますのでui-routerを利用するように変更します:

<div class="header">
  <ul class="nav nav-pills pull-right">
    <li class="active"><a ui-sref="home">Home</a></li>
    <li><a ui-sref="about">About</a></li>
    <li><a ui-sref="contact">Contact</a></li>
  </ul>
  <h3 class="text-muted">webapp</h3>
</div>

これでルーティングの処理が追加されました:

f:id:albatrosary:20150106205544p:plain

f:id:albatrosary:20150106205551p:plain

f:id:albatrosary:20150106205557p:plain

メニュー表示を変える

メニューを押されカレントページとボタンのactiveカラーを揃えたいので今回はコントローラーを追加し制御したいと思います。ヘッダー部は

<div class="header" ng-controller="HeaderCtrl">
  <ul class="nav nav-pills pull-right">
    <li ng-class="{active: menu['home']}"><a ui-sref="home" ng-click="onClick('home')">Home</a></li>
    <li ng-class="{active: menu['about']}"><a ui-sref="about" ng-click="onClick('about')">About</a></li>
    <li ng-class="{active: menu['contact']}"><a ui-sref="contact" ng-click="onClick('contact')">Contact</a></li>
  </ul>
  <h3 class="text-muted">webapp</h3>
</div>

このコントローラーをheader.htmlと同じところに定義(header.controller.jsとして)します:

'use strict';

angular.module('webapp')
  .controller('HeaderCtrl', ['$scope',
  function ($scope) {
    var MenuProperties = {'home':false, 'about':false, 'contact':false};
    $scope.menu = [];

    for (var prop in MenuProperties) {
      $scope.menu[prop] = MenuProperties[prop];
    }
    $scope.menu['home'] = true;
    $scope.onClick = function (pages) {
      for (var prop in MenuProperties) {
        $scope.menu[prop] = MenuProperties[prop];
      }
      $scope.menu[pages] = true;
    };
  }]);

ここまで完成

これでひと通り機能の追加は終わりです。結果としては次のようになります:

f:id:albatrosary:20150106210413p:plain

f:id:albatrosary:20150106210420p:plain

f:id:albatrosary:20150106210424p:plain

ビルド

ビルドを実行しビルド後の結果をブラウザで確認するためには次のコマンドを実行します

$ grunt serve:dist

現状だとエラーが発生するはずです:

f:id:albatrosary:20150106210709p:plain

HTMLがdistに含まれていないためです。修正するためにはGruntfile.jsを修正します。315行目あたりにcopy句がありますので次のように修正します:

            'components/{,*/}*.html',
            'scripts/{,*/}*.html',

を追加しています:

    // Copies remaining files to places other tasks can use
    copy: {
      dist: {
        files: [{
          expand: true,
          dot: true,
          cwd: '<%= config.app %>',
          dest: '<%= config.dist %>',
          src: [
            '*.{ico,png,txt}',
            'images/{,*/}*.webp',
            '{,*/}*.html',
            'components/{,*/}*.html',
            'scripts/{,*/}*.html',
            'styles/fonts/{,*/}*.*'
          ]
        }, {
          src: 'node_modules/apache-server-configs/dist/.htaccess',
          dest: '<%= config.dist %>/.htaccess'
        }, {
          expand: true,
          dot: true,
          cwd: '.',
          src: 'bower_components/bootstrap-sass-official/assets/fonts/bootstrap/*',
          dest: '<%= config.dist %>'
        }]
      },
      styles: {
        expand: true,
        dot: true,
        cwd: '<%= config.app %>/styles',
        dest: '.tmp/styles/',
        src: '{,*/}*.css'
      }
    },

これで無事ビルド後のモジュールでも表示されたと思います。

最後に

webappから必要なモジュールを追加することで、どのモジュールがどのような役割をしているのか理解することができました。その他細かい理解をするには良い方法だと思います。今回の作業ではkarmaでのテストを行ったませんのでまだまだ作業としてはやることが多いです。
サブジェネレーターを利用できないためそういったカスタマイズをジェネレータにする必要もあります。学習という意味合いにおいては非常に有効ではないかと実感しました。
参考までに完成したモジュールは下記にあります:

albatrosary/webapp-add-angular · GitHub

ここまでするならgenerator-generatorでいいのではという意見もありますが、今回は学習目的でしたので良しとします。