Help us understand the problem. What is going on with this article?

AngularJSアンチパターン集

More than 3 years have passed since last update.

(150522追記)本稿の続編としてAngularJSモダンプラクティスを掲載しました。本稿は2014年9月に執筆し、情報がかなり古くなっています。続編では、AngularJS 1.4やAngular 2に関する情報をまとめ、入門者への新鮮なチュートリアル、熟練者の移行手引として作成しました。どうぞご覧ください。

この記事は記録のため残します。


AngularJS歴1年の筆者による個人的なAngularJSアンチパターン集です。自分のための戒めとメモを兼ねています。個人差があると思いますので、参考程度に。

また、筆者はTypeScriptで書いています。

Components

ComponentsのDI数が6以上になる

危険度★★★

angular.module('myApp')
.service('FooService', [
  '$q',
  '$resource',
  '$rootScope',
  'OtherServiceA',
  'OtherServiceB',
  'OtherServiceC',
  'OtherServiceD',
  FooService
]);

ComponentsとはController, Service, Factoryなどのこと。規模が大きくなるにつれて起こりがち。6以上という数字は、明確な根拠は示せないがこれまでの実感から。5つ目を加えた辺りから嫌な予感をした方がいい。

なぜ?

DI数が6以上になるとそのComponentに負わせている責務が多すぎる証拠。いわゆる肥えたコントローラ。全体の見通しが悪くなりテスト時に必要なモックの種類も増え、面倒になってくる。

他のControllerを作ったときに同じ機能が必要になるケースが多いので、再利用性も考えServiceに切り出しておきたい。

対策

各種メソッドがどのComponentを使っているのか、使っていないのかを明確にし、共通しているメソッドを早々にServiceに括り委譲する。これだけで、だいたいDIが2つほど減らせる。

DIアノテーションを書かない

危険度★★★

angular.module('myApp')
.service('FooService', function($q, $resource, $rootScope) {
  // ...
});

なぜ?

AngularJSでは引数名をパースして依存するComponentを決定するが、このままではminify時に崩れてしまう。

対策

この問題を回避するため、常にDIアノテーションを書くか、アノテーション記述支援のビルドツールを用いる。AngularJS 1.3以上ならng-appと共にng-strict-diを指定することでアノテーションを忘れたときに警告がでる。

ビルドツールはng-annotateなど。本稿では割愛。

angular.module('myApp')
.service('FooService', [
  '$q',
  '$resource',
  '$rootScope',
  FooService
]);

Controllerが直接$resourceに依存する

危険度★★★

function ExampleCtrl($resource)
{
  // ...
}

まず間違いなくアンチパターン。コアサービスである$resourceはControllerで直接使わず、真っ先にServiceに括るべき。(コアサービスとは、ここではngモジュールと、angular-resource.jsなどのng名前空間モジュールを指す)

なぜ?

サーバーAPIからデータを取ってくる行動は複数のControllerで起こりうる。Controllerで直接$resourceを扱うと、重複コードが生まれる原因となりやすい。

APIのURLの管理やパラメータの管理など一元的に書く情報も多いので、Serviceに不慣れな初期段階でも、せめてやっておく。

対策

Controller内で$resourceを使うメソッドをServiceに切り出す。他にも、全体的に$で始まるコアサービスは何かとServiceでラップすることが多い。ラップするといっても次のことを言ってるわけではない。

angular.module('myApp')
.factory('MyResource', ['$resource', function ($resource) {
  return $resource;
}]);

例としてはこんな感じ (Plunker)

非同期処理をControllerでも書きたい場合、大抵は$promiseを返せば済むが、連鎖的に複雑なことをしたいときは$qを併用する。

主観によるラップすべきコアサービスと、Controllerでも使うコアサービス

  • Serviceでラップすべき
    • $q, $http, $location, $rootElement
    • $cookies, $resource
  • Directiveでラップすべき
    • $compile, $document, $interpolate, $parse
  • Controllerでも使う
    • $filter, $interval, $log, $rootScope, $scope, $timeout
    • $routeParams(Serviceに括ることも多い)
    • $window(DOMやGUI周りを触るならDirective)

サーバー取得データをControllerの変数に格納

危険度★☆☆

ts
class ExampleCtrl
{
  public data;

  // ...
}

$resourceをServiceに括っても、取得データをControllerに持たせるのは良くない。

なぜ?

Paginationといった変遷やルーティングによる画面の切り替えで「同じControllerを再表示するとき」にも初期化され、変数がundefinedになる瞬間が生まれる。画面上では一瞬表示が消える。

対策

Controllerインスタンスの破棄に関わらずデータを保持しておくための"SharedStore" Serviceを用意する。これはシングルトンだがグローバル変数ではなく、このServiceに依存しているComponentのみが情報を知りうる。画面上ではデータが保持され続けるので、表示が消えることもない。

なお、$resourceを用いるServiceとSharedStoreをひとまとめにするのも良くない(取得と保持は分離)。

反論

Controllerの変数に持たせて、$routeProviderresolveを使えば画面から一瞬消える現象を防げるのでは。

この件については次を参照。

$routeProviderのresolveを使う

危険度★☆☆

angular.module('myApp')
.config(function($routeProvider) {
  $routeProvider.when('/', {
    templateUrl: 'sample.html',
    controller: /* ... */ ,
    resolve: {
      /* ... */
    }
  });
});

$routeProviderresolveは長期的にみるとアンチパターン候補。使用する場合、問題を理解して使ったほうがよい。

なぜ?

現在のバージョン(AngularJS 1.2, 1.3系)のルート設定は頑固で、Controllerの実装が本体とConfigのresolveに散らばってしまう。そして考えずに書くとテストが困難となりやすく、DIアノテーションを別途用意する必要があるなど、運用直前に気付く問題が意外と潜んでいる。

対策

筆者個人的な感想としては、resolveよりも前述SharedStore Serviceを用いたほうがテスト、エラーハンドリング、再利用性の面で優れている。resolveはUX直結な要素なので、他で確保できれば別にこの機能にこだわる必要もないだろう。

サーバー取得データをService内で操作する

危険度★☆☆

Serviceの中で取得データ(例えば配列)を操作するのはアンチパターン候補。

なぜ?

AngularJSのAPIを使用しない純粋なJSのロジックがロックインされる。

対策

純粋なJSのロジックは独自ライブラリとしてAngularJSの外で書く。ただし、そのライブラリを用いるときは直接グローバル変数から持ってこず、一度Factoryにラップする。Factoryラップは、Serviceのテスト時にモックを使用しやすくするため。

angular.module('myApp')
.factory('myLibrary', function () {
  return myLibrary;
});

例外

この件は100%守るには厳しすぎる。どこまでService内でやるか、どこから独自ライブラリにするかは感覚的な問題。プロジェクトの方針にもよるだろう。

少しの操作なら構わないと思うが、数十行に渡る処理の中に一度もAngularJS APIが登場しなかったら、独自ライブラリを検討し始めるべき。

View

ControllerAs表記を用いない

危険度☆☆☆

賛否両論あります。

<div ng-controller="ExampleCtrl"></div>
<div ng-controller="ExampleCtrl as ex"></div>

なぜ?

複数のControllerがネストしたときに参照元で混乱が起きる。ネストしないと思っていてもDirectiveが増えるとまず起こりうる。

メリット

参照元が一目瞭然となる。$scopeをController内で使用する頻度が激減する。それにより$scope本来のAPI($watch$broadcastなど)を利用するときのみ意識が向けられる。

同じ対象についてのng-showやng-hideを3回以上書く

危険度★★☆

例えばログイン時と未ログイン時で表示を変える場合、さらにそれをヘッダとフッタに同じように書く場合。2回でも注意、3回だと今後見落とし発生の恐れあり。

なぜ?

ViewのHTML内はJS (TS) ソース以上に見通しが悪くなりがち。HTMLは従来のWebデザインと同じマークアップと捉えず、もはやソースコードと捉えるべき。一度量産してしまったHTMLの要素群をリファクタリングするのはJavaScriptをリファクタリングするより面倒。コピペに手が伸びたら思い留まる。

対策

AngularJSの鬼門とされるDirectiveのコツを早めに掴み、細かい部品も一個一個ケチらずDirectiveにする。「これはDirectiveにするほどでもないかな」と思ったものほどDirectiveにする価値がある。

Directiveにする例

  1. 同じ条件式のng-showng-hideをよく書く
    • Directive化が様々な事情で出来ないならば、せめて条件式を関数にラップして、その関数を評価させる。
    • ng-showng-hideを毎回ペアで使うようならば、そのペアをテンプレートにする。ngModelrequireに指定してng-model属性からデータのバインドが可能。独自Scopeを作っても問題ないが、ngModelControllerを利用すると簡便。
  2. Bootstrapを利用していて<div><div><div></div></div></div>のようなdiv入れ子が続いたとき。
    • ネストが浅くなるようテンプレートDirectiveを導入。テンプレートのみ利用、複雑な処理はしなくていい。transcludeを覚えると早い。
  3. 特定の表と動的に表示が変わる見出しが常にペアな場合。<h3>動的な見出し</h3><table><tr ng-repeat="...
    • <h3>{{head}}</h3>と書きがちだが、見出しと表をまとめてDirectiveに括りバインドを1箇所に限定すると、後々のデザイン変更、要素追加(件数表示、選択中表示など)に対応しやすい。
  4. パンくずリスト
    • 前述の表と似たような理由。パンくずリストは多くのViewで使うため、ng-repeatを隠蔽しておくとデザイン変更にも強く、扱いやすい。
  5. デバッグ用表示
    • <pre>{{ data | json }}</pre>とせずに、早めにデバッグ用のDirectiveを作っておく。
    • 開発時と運用時に、Directiveの変更で一斉に出力を変更できる。replaceにすれば表に出さないことも可。<pre>をひとつひとつ消して回ることはない。

Controllerのメソッドの引数に$eventを渡す

危険度★☆☆

<input ng-change="entry($event)">

Viewのng-clickng-changeなどで$eventを引数に指定するのは良くない。

なぜ?

$eventを使う状況はほとんどがキー入力周りかGUI周りである。これらをController内で処理すると確実に肥える。再利用性も低い。

再利用が考えられるシーンとしては、新規追加用のフォームと既存編集用のフォームでControllerを分ける場合、一画面に複数のControllerの編集リスト(お気に入りとフォロー)がある場合など。

対策

DOMに関するもの、UIに関するものはDirectiveに任せるのが最適。Directive内にもScopeとControllerを定義できるので、compilelinkだけでは捌けないほど複雑になってきたらそこで。もっと複雑になるなら更に部分的にServiceにする。

Controllerのメソッドの引数に$indexを渡すときは注意

危険度★★☆

<input ng-change="update($index)">

ng-repeatで列挙して、各要素から$index付きで処理をするときは対象となる配列に十分注意する。

なぜ?

FilterやSortを用いた場合などで、画面上の表示と格納されている配列の順番が食い違うことがある。そのまま処理すると意図していない対象が操作される。

対策

画面上の配列と処理に使う配列を同じものにする。(拙著の記事

コーディングスタイル

angular.module()のモジュール名が文字列リテラルのまま

危険度★☆☆

angular.module('myApp', ['ngRoute']);

module()に文字列のままモジュール名を記述しない。

var appName = 'myApp';
angular.module(appName, ['ngRoute']);

なぜ?

変更が必要になった場合の置換の手間だけでなく、変数名の場合TSコンパイラによるtypo指摘の恩恵を受けられる。JSの場合も最初に文字列で書いてしまうと、後々まで同じスタイルで書きがち。例示やチュートリアルからコピペすると起こりがち。

各種Provider名やController名も文字列リテラルを繰り返し書くより、変数にしておく。$routeProviderなどの記述で役に立つ。

$broadcastのnameを文字列リテラルで表記する

危険度★★★

<button ng-click="$broadcast('MyEvent')">Click Me</button>

なぜ?

$broadcast, $emit, $onのリスナー名を文字列リテラルで指定すると、typo時に一切の警告が無い。AngularJSはそれが間違っているか正しいのかを判別できないからである。

対策

Broadcast専門のServiceを作成し、リスナー名の管理や$broadcastの実行を一元化して各Componentからは隠蔽する。ViewのHTMLにも直接$broadcastなどと書かず、ServiceをラップしたControllerのメソッドから使う(LoDの観点から)。

後記

以上、1年間のリファクタリングや1から書き直しといった個人的経験、Stack Overflowや様々な情報源で得た知見、AngularJSリファレンス本を読んだ上での様々な反省を元に、特に面倒だった部分を挙げました。

関連リンク

--

思いついたらまた増やします。(14/9/16執筆、9/17改稿しました。ご意見、ご質問、ツッコミなど歓迎します)

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away