[AngularJS] 非 SPA モードにおける注意点

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

angularjs125title 車輪開発大好きおたいがです。こんにちは。( 挨拶 )

少し前の話ですが、非 SPA を要件とされた画面の構築に携わったので、そのときに感じた大切なポイントをまとめておきたいと思います。

ngRoute モジュールを使用しない

SPA の場合、元となる単一ページを開いたあとは、ハッシュ付き URL を切り替えることでサーバにリクエストを送ることなく画面遷移を実現しますが、非 SPA の場合は、画面遷移のたびにサーバにリクエストを投げて画面を再構築します。

そのため、ルーティング機能をもつ ngRoute モジュールを使用することはありませんでした。

ルーティング変更のタイミングをフックして、URL を見て、ロジックをゴリゴリ書く…といった処理を意識せず済む。という点では幾分か気が楽なるかもしれません。( あくまで個人の感想です )

$locationProvider の設定

AngularJS アプリケーションの config 定義処理内にて html5Mode.enabledtrue に、html5Mode.requireBasefalse に設定します。

angular.module('sampleApp', [], function() {
})
.config(function($locationProvider) {
  $locationProvider.html5Mode({
    'enabled'     : true,
    'requireBase' : false
  });
})
…

BASE 要素の定義

SPA では、画面の URL によってパスの階層が変わる可能性があるため、相対 URL の基点を固定しておく必要があります。

<BASE> 要素を使用することで、相対 URL の基点を固定することができます。サーバーサイドのテンプレートエンジンを利用してテンプレート継承を行う場合は、以下のようにルートで固定することになると思われます。

<head>
  …
  <base href="/" />
</head>

サーバから値を受け取る方法

SPA の場合は、画面構築後に RESTful API を呼び出して返ってきた値を画面に反映させることが常套手段となりますが、画面遷移ごとにサーバにリクエストを投げて画面を再構築する非 SPA の場合は、レスポンス時に画面のソースに必要な値を埋め込めるために、画面初期化時の実装手段がいくつか選択できると考えられます。

[1] SPA と同様に RESTful API を呼び出すパターン

毎回毎回、画面遷移でリクエストしたあとに RESTful API をリクエスト…可能ですが、個人的には採用したくない手段です。

[2] 各 DOM ごとに ng-init ディレクティブを定義して値を埋め込むパターン

割と使える手段だと思いました…

Thymeleaf で流し込む例

<input
  type="text"
  name="userName"
  ng-model="registerUser.userName"
  th:attr="ng-init='registerUser.userName=\''+${value}+'\''" />

ですが、テンプレートが冗長的になる上に、画面の仕様変更が入るとフロント側、サーバ側、いずれの担当者も心が擦り減る結果が見えてしまうので、採用の可否は慎重にすべきだと思います。( 実際に擦り減りました )

また、画面の規模によって ng-init の処理が期待していないタイミングで実行されることがあるようです。

[3] META 要素に値を埋め込み、コントローラで参照、代入するパターン

個人的にはイチオシでして、<HEAD> 要素内に <META> 要素をサーバ側で埋め込み、AngularJS のコントローラ側で jQuery を使用して参照するという手法です。先述の ng-init ディレクティブを使用した方法と違い、画面の表示に関わる DOM に影響がないため、気軽に要素を付け足すことが可能です。

実際に以下のように実装しました。

<head>
  <meta "cm:userId" value="12345" />
  <meta "cm:sample" value="hoge" />
  …
</head>
(function() {
  'use strict';
  angular
  .module('sampleApp')
  .constant('MetaElement', {
    /**
     * ユーザ ID
     * @const USER_ID
     * @type {String}
     */
    'USER_ID' : { 'get' : function() { return angular.element('meta[property=\'cm:userId\']').attr('value'); }},
    /**
     * サンプル ( ブログの記事用 )
     * @const SAMPLE
     * @type {String}
     */
    'SAMPLE' : { 'get' : function() { return angular.element('meta[property=\'cm:sample\']').attr('value'); }}
  });
}());


(function() {
  'use strict';
  angular
  .module('sampleApp')
  .controller('SampleController', function($scope, MetaElement) {
  
    var userId = MetaElement.USER_ID.get();
      
    var sample = MetaElement.SAMPLE.get();
  
  });
}());

ng-template および $compile, $templateCache サービスの利用

サーバサイドのテンプレート単位で ng-template のモジュール (HTML) を作成して各画面にインポートします。画面上では単なるテキストとしてレンダリングされますが、$templateCache, $compile サービスを利用することでサーバの値が埋め込まれた状態の汎用テンプレートモジュールが容易に作成できます。

必須ではありませんが、自分が携わった案件ではかなり使用していました。

<?xml version="1.0" encoding="UTF-8" ?>
<script
type = "text/ng-template"
id   = "HogeViewModule.html"
>
  <div ng-controller="HogeViewModuleController">
    …
  </div>
</script>
(function() {
  'use strict';
  angular
  .module('sampleApp')
  .controller('SampleController', function($scope, $compile, $templateCache) {
    var showModule = function (template, scope) {
      var jqBody = angular.element('body');
      module = $compile(template)(scope);
      jqBody.append(module);
    };
    var template = $templateCache.get('HogeViewModule.html');
    showModule(template, $scope);
  });
}());

実際に実装するときは、$templateCache, $compile などを利用した処理はサービスにラップすると良いと思います

まとめ

SPA における注意点をまとめました。SPA の場合と違い、サーバサイドを意識しながら実装するため、設計・実装方法が SPA の場合と異なる箇所が少なくありません。

当記事が、これから AngularJS の非 SPA 仕様のアプリケーションを実装する人のヒントになれば幸いです。