└── README.md
/README.md:
--------------------------------------------------------------------------------
1 | # AngularJS スタイルガイド
2 |
3 | *チーム開発のための AngularJS スタイルガイド by [@toddmotto](//twitter.com/toddmotto)*
4 |
5 | チームで AngularJS アプリケーションを開発するための標準的なスタイルガイドとして、Angular アプリケーションについてのこれまでの[記事](http:////toddmotto.com)、[講演](https://speakerdeck.com/toddmotto)、そして構築してきた経験を基にして、コンセプト、シンタックス、規約についてまとめている。
6 |
7 | #### コミュニティ
8 | [John Papa](//twitter.com/John_Papa) と私は Angular スタイルのパターンについて話し合い、それによってこのガイドはよりすばらしいものとなっている。それぞれのスタイルガイドをリリースしているので、考えを比較するためにも [John のスタイルガイド](//github.com/johnpapa/angularjs-styleguide)もぜひ確認してみてほしい。
9 |
10 | > See the [original article](http://toddmotto.com/opinionated-angular-js-styleguide-for-teams) that sparked this off
11 |
12 | ## Table of Contents
13 |
14 | 1. [Modules](#modules)
15 | 1. [Controllers](#controllers)
16 | 1. [Services and Factory](#services-and-factory)
17 | 1. [Directives](#directives)
18 | 1. [Filters](#filters)
19 | 1. [Routing resolves](#routing-resolves)
20 | 1. [Publish and subscribe events](#publish-and-subscribe-events)
21 | 1. [Performance](#performance)
22 | 1. [Angular wrapper references](#angular-wrapper-references)
23 | 1. [Comment standards](#comment-standards)
24 | 1. [Minification and annotation](#minification-and-annotation)
25 |
26 | ## Modules
27 |
28 | - **定義**: 変数を使わずに setter / getter で module を定義
29 |
30 | ```javascript
31 | // avoid
32 | var app = angular.module('app', []);
33 | app.controller();
34 | app.factory();
35 |
36 | // recommended
37 | angular
38 | .module('app', [])
39 | .controller()
40 | .factory();
41 | ```
42 |
43 | - Note: `angular.module('app', []);` を setter、`angular.module('app');` を getter として使う。setter で module を定義し、他のインスタンスからは getter でその module を取得して利用する。
44 |
45 | - **メソッド**: コールバックとして記述せず、function を定義してメソッドに渡す
46 |
47 | ```javascript
48 | // avoid
49 | angular
50 | .module('app', [])
51 | .controller('MainCtrl', function MainCtrl () {
52 |
53 | })
54 | .service('SomeService', function SomeService () {
55 |
56 | });
57 |
58 | // recommended
59 | function MainCtrl () {
60 |
61 | }
62 | function SomeService () {
63 |
64 | }
65 | angular
66 | .module('app', [])
67 | .controller('MainCtrl', MainCtrl)
68 | .service('SomeService', SomeService);
69 | ```
70 |
71 | - コードのネストが深くなることを抑え、可読性を高められる
72 |
73 | - **IIFE(イッフィー:即時関数式)スコープ**: Angular に渡す function の定義でグローバルスコープを汚染することを避けるため、複数ファイルを連結(concatenate)するビルドタスクで IIFE 内にラップする
74 |
75 | ```javascript
76 | (function () {
77 |
78 | angular
79 | .module('app', []);
80 |
81 | // MainCtrl.js
82 | function MainCtrl () {
83 |
84 | }
85 |
86 | angular
87 | .module('app')
88 | .controller('MainCtrl', MainCtrl);
89 |
90 | // SomeService.js
91 | function SomeService () {
92 |
93 | }
94 |
95 | angular
96 | .module('app')
97 | .service('SomeService', SomeService);
98 |
99 | // ...
100 |
101 | })();
102 | ```
103 |
104 |
105 | **[Back to top](#table-of-contents)**
106 |
107 | ## Controllers
108 |
109 | - **controllerAs**: Controller はクラスであるため、常に `controllerAs` を利用する
110 |
111 | ```html
112 |
113 |
114 | {{ someObject }}
115 |
116 |
117 |
118 |
119 | {{ main.someObject }}
120 |
121 | ```
122 |
123 | - DOM で controller ごとに変数を定義し、`$parent` の利用を避ける
124 |
125 | - `controllerAs` では controller 内で `$scope` にバインドされる `this` を利用する
126 |
127 | ```javascript
128 | // avoid
129 | function MainCtrl ($scope) {
130 | $scope.someObject = {};
131 | $scope.doSomething = function () {
132 |
133 | };
134 | }
135 |
136 | // recommended
137 | function MainCtrl () {
138 | this.someObject = {};
139 | this.doSomething = function () {
140 |
141 | };
142 | }
143 | ```
144 |
145 | - `$emit`、`$broadcast`、`$on` や `$watch` で必要とならない限り、`controllerAs` では `$scope` を利用しない
146 |
147 | - **継承**: controller クラスを拡張する場合は prototype 継承を利用する
148 |
149 | ```javascript
150 | function BaseCtrl () {
151 | this.doSomething = function () {
152 |
153 | };
154 | }
155 | BaseCtrl.prototype.someObject = {};
156 | BaseCtrl.prototype.sharedSomething = function () {
157 |
158 | };
159 |
160 | AnotherCtrl.prototype = Object.create(BaseCtrl.prototype);
161 |
162 | function AnotherCtrl () {
163 | this.anotherSomething = function () {
164 |
165 | };
166 | }
167 | ```
168 |
169 | - `Object.create` をレガシーブラウザでもサポートするためには polyfill を利用する
170 |
171 | - **controllerAs 'vm'**: controller の `this` コンテキストを、`ViewModel` を意味する `vm` として保持する
172 |
173 | ```javascript
174 | // avoid
175 | function MainCtrl () {
176 | this.doSomething = function () {
177 |
178 | };
179 | }
180 |
181 | // recommended
182 | function MainCtrl (SomeService) {
183 | var vm = this;
184 | vm.doSomething = SomeService.doSomething;
185 | }
186 | ```
187 |
188 | *Why?* : Function コンテキストが `this` の値を変えてしまうことによる `.bind()` の利用とスコープの問題を回避するため
189 |
190 | - **プレゼンテーションロジックのみ (MVVM)**: controller 内ではプレゼンテーションロジックのみとし、ビジネスロジックは service に委譲する
191 |
192 | ```javascript
193 | // avoid
194 | function MainCtrl () {
195 |
196 | var vm = this;
197 |
198 | $http
199 | .get('/users')
200 | .success(function (response) {
201 | vm.users = response;
202 | });
203 |
204 | vm.removeUser = function (user, index) {
205 | $http
206 | .delete('/user/' + user.id)
207 | .then(function (response) {
208 | vm.users.splice(index, 1);
209 | });
210 | };
211 |
212 | }
213 |
214 | // recommended
215 | function MainCtrl (UserService) {
216 |
217 | var vm = this;
218 |
219 | UserService
220 | .getUsers()
221 | .then(function (response) {
222 | vm.users = response;
223 | });
224 |
225 | vm.removeUser = function (user, index) {
226 | UserService
227 | .removeUser(user)
228 | .then(function (response) {
229 | vm.users.splice(index, 1);
230 | });
231 | };
232 |
233 | }
234 | ```
235 |
236 | *Why?* : controller では service からモデルのデータを取得するようにしてビジネスロジックを避け、ViewModel としてモデル・ビュー間のデータフローを制御させる。controller 内のビジネスロジックは service のテストを不可能にしてしまう。
237 |
238 | **[Back to top](#table-of-contents)**
239 |
240 | ## Services and Factory
241 |
242 | - すべての Angular Services はシングルトンで、`.service()` と `.factory()` はオブジェクトの生成され方が異なる
243 |
244 | **Services**: `constructor` function として `new` で生成し、パブリックなメソッドと変数に `this` を使う
245 |
246 | ```javascript
247 | function SomeService () {
248 | this.someMethod = function () {
249 |
250 | };
251 | }
252 | angular
253 | .module('app')
254 | .service('SomeService', SomeService);
255 | ```
256 |
257 | **Factory**: ビジネスロジックやプロバイダモジュールで、オブジェクトやクロージャを返す
258 |
259 | - 常にホストオブジェクトを返す
260 |
261 | ```javascript
262 | function AnotherService () {
263 | var AnotherService = {};
264 | AnotherService.someValue = '';
265 | AnotherService.someMethod = function () {
266 |
267 | };
268 | return AnotherService;
269 | }
270 | angular
271 | .module('app')
272 | .factory('AnotherService', AnotherService);
273 | ```
274 |
275 | *Why?* : "Revealing Module Pattern" では primitive な値は更新されない
276 |
277 | **[Back to top](#table-of-contents)**
278 |
279 | ## Directives
280 |
281 | - **restrict**: 独自 directive には `custom element` と `custom attribute` のみ利用する(`{ restrict: 'EA' }`)
282 |
283 | ```html
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 | ```
294 |
295 | - コメントとクラス名での宣言は混乱しやすいため使うべきでない。コメントでの宣言は古いバージョンの IE では動作せず、属性での宣言が古いブラウザをカバーするのにもっとも安全である。
296 |
297 | - **template**: テンプレートをすっきりさせるために `Array.join('')` を利用する
298 |
299 | ```javascript
300 | // avoid
301 | function someDirective () {
302 | return {
303 | template: '' +
304 | '
My directive
' +
305 | ''
306 | };
307 | }
308 |
309 | // recommended
310 | function someDirective () {
311 | return {
312 | template: [
313 | '',
314 | '
My directive
',
315 | ''
316 | ].join('')
317 | };
318 | }
319 | ```
320 |
321 | *Why?* : 適切なインデントでコードの可読性を高められ、不適切に `+` を使ってしまうことによるエラーを避けられる
322 |
323 | - **DOM 操作**: directive 内のみとし、controller / service では DOM を操作しない
324 |
325 | ```javascript
326 | // avoid
327 | function UploadCtrl () {
328 | $('.dragzone').on('dragend', function () {
329 | // handle drop functionality
330 | });
331 | }
332 | angular
333 | .module('app')
334 | .controller('UploadCtrl', UploadCtrl);
335 |
336 | // recommended
337 | function dragUpload () {
338 | return {
339 | restrict: 'EA',
340 | link: function ($scope, $element, $attrs) {
341 | $element.on('dragend', function () {
342 | // handle drop functionality
343 | });
344 | }
345 | };
346 | }
347 | angular
348 | .module('app')
349 | .directive('dragUpload', dragUpload);
350 | ```
351 |
352 | - **命名規約**: 将来的に標準 directive と名前が衝突する可能性があるため、`ng-*` を独自 directive に使わない
353 |
354 | ```javascript
355 | // avoid
356 | //
357 | function ngUpload () {
358 | return {};
359 | }
360 | angular
361 | .module('app')
362 | .directive('ngUpload', ngUpload);
363 |
364 | // recommended
365 | //
366 | function dragUpload () {
367 | return {};
368 | }
369 | angular
370 | .module('app')
371 | .directive('dragUpload', dragUpload);
372 | ```
373 |
374 | - directive と filter は先頭文字を小文字で命名する。これは、Angular が `camelCase` をハイフンつなぎとする命名規約によるもので、`dragUpload` が要素で使われた場合は `` となる。
375 |
376 | - **controllerAs**: directive でも `controllerAs` を使う
377 |
378 | ```javascript
379 | // avoid
380 | function dragUpload () {
381 | return {
382 | controller: function ($scope) {
383 |
384 | }
385 | };
386 | }
387 | angular
388 | .module('app')
389 | .directive('dragUpload', dragUpload);
390 |
391 | // recommended
392 | function dragUpload () {
393 | return {
394 | controllerAs: 'dragUpload',
395 | controller: function () {
396 |
397 | }
398 | };
399 | }
400 | angular
401 | .module('app')
402 | .directive('dragUpload', dragUpload);
403 | ```
404 |
405 | **[Back to top](#table-of-contents)**
406 |
407 | ## Filters
408 |
409 | - **グローバル filter**: angular.filter() を使ってグローバルな filter を作成し、controller / service 内でローカルな filter を使わない
410 |
411 | ```javascript
412 | // avoid
413 | function SomeCtrl () {
414 | this.startsWithLetterA = function (items) {
415 | return items.filter(function (item) {
416 | return /^a/i.test(item.name);
417 | });
418 | };
419 | }
420 | angular
421 | .module('app')
422 | .controller('SomeCtrl', SomeCtrl);
423 |
424 | // recommended
425 | function startsWithLetterA () {
426 | return function (items) {
427 | return items.filter(function (item) {
428 | return /^a/i.test(item.name);
429 | });
430 | };
431 | }
432 | angular
433 | .module('app')
434 | .filter('startsWithLetterA', startsWithLetterA);
435 | ```
436 |
437 | - テストのしやすさと再利用性を高めるため
438 |
439 | **[Back to top](#table-of-contents)**
440 |
441 | ## Routing resolves
442 |
443 | - **Promises**: `$routeProvider`(または `ui-router` の `$stateProvider`)内で controller の依存を解決する
444 |
445 | ```javascript
446 | // avoid
447 | function MainCtrl (SomeService) {
448 | var _this = this;
449 | // unresolved
450 | _this.something;
451 | // resolved asynchronously
452 | SomeService.doSomething().then(function (response) {
453 | _this.something = response;
454 | });
455 | }
456 | angular
457 | .module('app')
458 | .controller('MainCtrl', MainCtrl);
459 |
460 | // recommended
461 | function config ($routeProvider) {
462 | $routeProvider
463 | .when('/', {
464 | templateUrl: 'views/main.html',
465 | resolve: {
466 | // resolve here
467 | }
468 | });
469 | }
470 | angular
471 | .module('app')
472 | .config(config);
473 | ```
474 |
475 | - **Controller.resolve プロパティ**: ロジックを router にバインドせず、controller の `resolve` プロパティでロジックを関連付ける
476 |
477 | ```javascript
478 | // avoid
479 | function MainCtrl (SomeService) {
480 | this.something = SomeService.something;
481 | }
482 |
483 | function config ($routeProvider) {
484 | $routeProvider
485 | .when('/', {
486 | templateUrl: 'views/main.html',
487 | controllerAs: 'main',
488 | controller: 'MainCtrl'
489 | resolve: {
490 | doSomething: function () {
491 | return SomeService.doSomething();
492 | }
493 | }
494 | });
495 | }
496 |
497 | // recommended
498 | function MainCtrl (SomeService) {
499 | this.something = SomeService.something;
500 | }
501 |
502 | MainCtrl.resolve = {
503 | doSomething: function (SomeService) {
504 | return SomeService.doSomething();
505 | }
506 | };
507 |
508 | function config ($routeProvider) {
509 | $routeProvider
510 | .when('/', {
511 | templateUrl: 'views/main.html',
512 | controllerAs: 'main',
513 | controller: 'MainCtrl'
514 | resolve: MainCtrl.resolve
515 | });
516 | }
517 | ```
518 |
519 | - こうすることで controller と同じファイル内に resolve の依存を持たせ、router をロジックから開放する
520 |
521 | **[Back to top](#table-of-contents)**
522 |
523 | ## Publish and subscribe events
524 |
525 | - **$scope**: scope 間をつなぐイベントトリガーとして `$emit` と `$broadcast` を使う
526 |
527 | ```javascript
528 | // up the $scope
529 | $scope.$emit('customEvent', data);
530 |
531 | // down the $scope
532 | $scope.$broadcast('customEvent', data);
533 | ```
534 |
535 | - **$rootScope**: アプリケーション全体のイベントとして `$emit` を使い、忘れずにリスナーをアンバインドする
536 |
537 | ```javascript
538 | // all $rootScope.$on listeners
539 | $rootScope.$emit('customEvent', data);
540 | ```
541 |
542 | - ヒント: `$rootScope.$on` リスナーは、`$scope.$on` リスナーと異なり常に残存するため、関連する `$scope` が `$destroy` イベントを発生させたときに破棄する必要がある
543 |
544 | ```javascript
545 | // call the closure
546 | var unbind = $rootScope.$on('customEvent'[, callback]);
547 | $scope.$on('$destroy', unbind);
548 | ```
549 |
550 | - `$rootScope` リスナーが複数ある場合は、Object リテラルでループして `$destroy` イベント時に自動的にアンバインドさせる
551 |
552 | ```javascript
553 | var rootListeners = {
554 | 'customEvent1': $rootScope.$on('customEvent1'[, callback]),
555 | 'customEvent2': $rootScope.$on('customEvent2'[, callback]),
556 | 'customEvent3': $rootScope.$on('customEvent3'[, callback])
557 | };
558 | for (var unbind in rootListeners) {
559 | $scope.$on('$destroy', rootListeners[unbind]);
560 | }
561 | ```
562 |
563 | **[Back to top](#table-of-contents)**
564 |
565 | ## Performance
566 |
567 | - **ワンタイムバインド**: Angular の新しいバージョン(v1.3.0-beta.10+)では、ワンタイムバインドのシンタックス `{{ ::value }}` を利用する
568 |
569 | ```html
570 | // avoid
571 | {{ vm.title }}
572 |
573 | // recommended
574 | {{ ::vm.title }}
575 | ```
576 |
577 | *Why?* : `undefined` の変数が解決されたときに `$$watchers` から取り除き、ダーティチェックでのパフォーマンスを改善する
578 |
579 | - **$scope.$digest を検討**: `$scope.$apply` でなく `$scope.$digest` を使い、子スコープのみを更新する
580 |
581 | ```javascript
582 | $scope.$digest();
583 | ```
584 |
585 | *Why?* : `$scope.$apply` は `$rootScope.$digest` を呼び出すため、アプリケーション全体の `$$watchers` をダーティチェックするが、`$scope.$digest` は `$scope` のスコープと子スコープを更新する
586 |
587 | **[Back to top](#table-of-contents)**
588 |
589 | ## Angular wrapper references
590 |
591 | - **$document と $window**: `$document` と `$window` を常に利用する
592 |
593 | ```javascript
594 | // avoid
595 | function dragUpload () {
596 | return {
597 | link: function ($scope, $element, $attrs) {
598 | document.addEventListener('click', function () {
599 |
600 | });
601 | }
602 | };
603 | }
604 |
605 | // recommended
606 | function dragUpload () {
607 | return {
608 | link: function ($scope, $element, $attrs, $document) {
609 | $document.addEventListener('click', function () {
610 |
611 | });
612 | }
613 | };
614 | }
615 | ```
616 |
617 | - **$timeout と $interval**: Angular の双方向データバインドが最新の状態を維持するよう `$timeout` と `$interval` を利用する
618 |
619 | ```javascript
620 | // avoid
621 | function dragUpload () {
622 | return {
623 | link: function ($scope, $element, $attrs) {
624 | setTimeout(function () {
625 | //
626 | }, 1000);
627 | }
628 | };
629 | }
630 |
631 | // recommended
632 | function dragUpload ($timeout) {
633 | return {
634 | link: function ($scope, $element, $attrs) {
635 | $timeout(function () {
636 | //
637 | }, 1000);
638 | }
639 | };
640 | }
641 | ```
642 |
643 | **[Back to top](#table-of-contents)**
644 |
645 | ## Comment standards
646 |
647 | - **jsDoc**: jsDoc で function 名、説明、パラメータ、返り値をドキュメント化する
648 |
649 | ```javascript
650 | /**
651 | * @name SomeService
652 | * @desc Main application Controller
653 | */
654 | function SomeService (SomeService) {
655 |
656 | /**
657 | * @name doSomething
658 | * @desc Does something awesome
659 | * @param {Number} x First number to do something with
660 | * @param {Number} y Second number to do something with
661 | * @returns {Number}
662 | */
663 | this.doSomething = function (x, y) {
664 | return x * y;
665 | };
666 |
667 | }
668 | angular
669 | .module('app')
670 | .service('SomeService', SomeService);
671 | ```
672 |
673 | **[Back to top](#table-of-contents)**
674 |
675 | ## Minification and annotation
676 |
677 | - **ng-annotate**: `ng-min` は deprecated なので、[ng-annotate](//github.com/olov/ng-annotate) for Gulp を利用し、`/** @ngInject */` で function にコメントして自動的に DI (dependency injection) させる
678 |
679 | ```javascript
680 | /**
681 | * @ngInject
682 | */
683 | function MainCtrl (SomeService) {
684 | this.doSomething = SomeService.doSomething;
685 | }
686 | angular
687 | .module('app')
688 | .controller('MainCtrl', MainCtrl);
689 | ```
690 |
691 | - 以下のような `$inject` アノテーションを含む出力となる
692 |
693 | ```javascript
694 | /**
695 | * @ngInject
696 | */
697 | function MainCtrl (SomeService) {
698 | this.doSomething = SomeService.doSomething;
699 | }
700 | MainCtrl.$inject = ['SomeService'];
701 | angular
702 | .module('app')
703 | .controller('MainCtrl', MainCtrl);
704 | ```
705 |
706 | **[Back to top](#table-of-contents)**
707 |
708 | ## Angular docs
709 | その他、API リファレンスなどの情報は、[Angular documentation](//docs.angularjs.org/api) を確認する。
710 |
711 | ## Contributing
712 |
713 | Open an issue first to discuss potential changes/additions.
714 |
715 | ## License
716 |
717 | #### (The MIT License)
718 |
719 | Copyright (c) 2014 Todd Motto
720 |
721 | Permission is hereby granted, free of charge, to any person obtaining
722 | a copy of this software and associated documentation files (the
723 | 'Software'), to deal in the Software without restriction, including
724 | without limitation the rights to use, copy, modify, merge, publish,
725 | distribute, sublicense, and/or sell copies of the Software, and to
726 | permit persons to whom the Software is furnished to do so, subject to
727 | the following conditions:
728 |
729 | The above copyright notice and this permission notice shall be
730 | included in all copies or substantial portions of the Software.
731 |
732 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
733 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
734 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
735 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
736 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
737 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
738 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
739 |
--------------------------------------------------------------------------------