└── 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 | --------------------------------------------------------------------------------