- Notifications
You must be signed in to change notification settings - Fork4
AngularJS styleguide for teams
tama3bb/angularjs-styleguide
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
チーム開発のための AngularJS スタイルガイド by@toddmotto
チームで AngularJS アプリケーションを開発するための標準的なスタイルガイドとして、Angular アプリケーションについてのこれまでの記事、講演、そして構築してきた経験を基にして、コンセプト、シンタックス、規約についてまとめている。
John Papa と私は Angular スタイルのパターンについて話し合い、それによってこのガイドはよりすばらしいものとなっている。それぞれのスタイルガイドをリリースしているので、考えを比較するためにもJohn のスタイルガイドもぜひ確認してみてほしい。
See theoriginal article that sparked this off
- Modules
- Controllers
- Services and Factory
- Directives
- Filters
- Routing resolves
- Publish and subscribe events
- Performance
- Angular wrapper references
- Comment standards
- Minification and annotation
定義: 変数を使わずに setter / getter で module を定義
// avoidvarapp=angular.module('app',[]);app.controller();app.factory();// recommendedangular.module('app',[]).controller().factory();
Note:
angular.module('app', []);を setter、angular.module('app');を getter として使う。setter で module を定義し、他のインスタンスからは getter でその module を取得して利用する。メソッド: コールバックとして記述せず、function を定義してメソッドに渡す
// avoidangular.module('app',[]).controller('MainCtrl',functionMainCtrl(){}).service('SomeService',functionSomeService(){});// recommendedfunctionMainCtrl(){}functionSomeService(){}angular.module('app',[]).controller('MainCtrl',MainCtrl).service('SomeService',SomeService);
コードのネストが深くなることを抑え、可読性を高められる
IIFE(イッフィー:即時関数式)スコープ: Angular に渡す function の定義でグローバルスコープを汚染することを避けるため、複数ファイルを連結(concatenate)するビルドタスクで IIFE 内にラップする
(function(){angular.module('app',[]);// MainCtrl.jsfunctionMainCtrl(){}angular.module('app').controller('MainCtrl',MainCtrl);// SomeService.jsfunctionSomeService(){}angular.module('app').service('SomeService',SomeService);// ...})();
controllerAs: Controller はクラスであるため、常に
controllerAsを利用する<!-- avoid --><divng-controller="MainCtrl"> {{ someObject }}</div><!-- recommended --><divng-controller="MainCtrl as main"> {{ main.someObject }}</div>
DOM で controller ごとに変数を定義し、
$parentの利用を避けるcontrollerAsでは controller 内で$scopeにバインドされるthisを利用する// avoidfunctionMainCtrl($scope){$scope.someObject={};$scope.doSomething=function(){};}// recommendedfunctionMainCtrl(){this.someObject={};this.doSomething=function(){};}
$emit、$broadcast、$onや$watchで必要とならない限り、controllerAsでは$scopeを利用しない継承: controller クラスを拡張する場合は prototype 継承を利用する
functionBaseCtrl(){this.doSomething=function(){};}BaseCtrl.prototype.someObject={};BaseCtrl.prototype.sharedSomething=function(){};AnotherCtrl.prototype=Object.create(BaseCtrl.prototype);functionAnotherCtrl(){this.anotherSomething=function(){};}
Object.createをレガシーブラウザでもサポートするためには polyfill を利用するcontrollerAs 'vm': controller の
thisコンテキストを、ViewModelを意味するvmとして保持する// avoidfunctionMainCtrl(){this.doSomething=function(){};}// recommendedfunctionMainCtrl(SomeService){varvm=this;vm.doSomething=SomeService.doSomething;}
Why? : Function コンテキストが
thisの値を変えてしまうことによる.bind()の利用とスコープの問題を回避するためプレゼンテーションロジックのみ (MVVM): controller 内ではプレゼンテーションロジックのみとし、ビジネスロジックは service に委譲する
// avoidfunctionMainCtrl(){varvm=this;$http.get('/users').success(function(response){vm.users=response;});vm.removeUser=function(user,index){$http.delete('/user/'+user.id).then(function(response){vm.users.splice(index,1);});};}// recommendedfunctionMainCtrl(UserService){varvm=this;UserService.getUsers().then(function(response){vm.users=response;});vm.removeUser=function(user,index){UserService.removeUser(user).then(function(response){vm.users.splice(index,1);});};}
Why? : controller では service からモデルのデータを取得するようにしてビジネスロジックを避け、ViewModel としてモデル・ビュー間のデータフローを制御させる。controller 内のビジネスロジックは service のテストを不可能にしてしまう。
- すべての Angular Services はシングルトンで、
.service()と.factory()はオブジェクトの生成され方が異なる
Services:constructor function としてnew で生成し、パブリックなメソッドと変数にthis を使う
```javascriptfunction SomeService () { this.someMethod = function () { };}angular .module('app') .service('SomeService', SomeService);```Factory: ビジネスロジックやプロバイダモジュールで、オブジェクトやクロージャを返す
常にホストオブジェクトを返す
functionAnotherService(){varAnotherService={};AnotherService.someValue='';AnotherService.someMethod=function(){};returnAnotherService;}angular.module('app').factory('AnotherService',AnotherService);
Why? : "Revealing Module Pattern" では primitive な値は更新されない
restrict: 独自 directive には
custom elementとcustom attributeのみ利用する({ restrict: 'EA' })<!-- avoid --><!-- directive: my-directive --><divclass="my-directive"></div><!-- recommended --><my-directive></my-directive><divmy-directive></div>
コメントとクラス名での宣言は混乱しやすいため使うべきでない。コメントでの宣言は古いバージョンの IE では動作せず、属性での宣言が古いブラウザをカバーするのにもっとも安全である。
template: テンプレートをすっきりさせるために
Array.join('')を利用する// avoidfunctionsomeDirective(){return{template:'<div>'+'<h1>My directive</h1>'+'</div>'};}// recommendedfunctionsomeDirective(){return{template:['<div>','<h1>My directive</h1>','</div>'].join('')};}
Why? : 適切なインデントでコードの可読性を高められ、不適切に
+を使ってしまうことによるエラーを避けられるDOM 操作: directive 内のみとし、controller / service では DOM を操作しない
// avoidfunctionUploadCtrl(){$('.dragzone').on('dragend',function(){// handle drop functionality});}angular.module('app').controller('UploadCtrl',UploadCtrl);// recommendedfunctiondragUpload(){return{restrict:'EA',link:function($scope,$element,$attrs){$element.on('dragend',function(){// handle drop functionality});}};}angular.module('app').directive('dragUpload',dragUpload);
命名規約: 将来的に標準 directive と名前が衝突する可能性があるため、
ng-*を独自 directive に使わない// avoid// <div ng-upload></div>functionngUpload(){return{};}angular.module('app').directive('ngUpload',ngUpload);// recommended// <div drag-upload></div>functiondragUpload(){return{};}angular.module('app').directive('dragUpload',dragUpload);
directive と filter は先頭文字を小文字で命名する。これは、Angular が
camelCaseをハイフンつなぎとする命名規約によるもので、dragUploadが要素で使われた場合は<div drag-upload></div>となる。controllerAs: directive でも
controllerAsを使う// avoidfunctiondragUpload(){return{controller:function($scope){}};}angular.module('app').directive('dragUpload',dragUpload);// recommendedfunctiondragUpload(){return{controllerAs:'dragUpload',controller:function(){}};}angular.module('app').directive('dragUpload',dragUpload);
グローバル filter: angular.filter() を使ってグローバルな filter を作成し、controller / service 内でローカルな filter を使わない
// avoidfunctionSomeCtrl(){this.startsWithLetterA=function(items){returnitems.filter(function(item){return/^a/i.test(item.name);});};}angular.module('app').controller('SomeCtrl',SomeCtrl);// recommendedfunctionstartsWithLetterA(){returnfunction(items){returnitems.filter(function(item){return/^a/i.test(item.name);});};}angular.module('app').filter('startsWithLetterA',startsWithLetterA);
テストのしやすさと再利用性を高めるため
Promises:
$routeProvider(またはui-routerの$stateProvider)内で controller の依存を解決する// avoidfunctionMainCtrl(SomeService){var_this=this;// unresolved_this.something;// resolved asynchronouslySomeService.doSomething().then(function(response){_this.something=response;});}angular.module('app').controller('MainCtrl',MainCtrl);// recommendedfunctionconfig($routeProvider){$routeProvider.when('/',{templateUrl:'views/main.html',resolve:{// resolve here}});}angular.module('app').config(config);
Controller.resolve プロパティ: ロジックを router にバインドせず、controller の
resolveプロパティでロジックを関連付ける// avoidfunctionMainCtrl(SomeService){this.something=SomeService.something;}functionconfig($routeProvider){$routeProvider.when('/',{templateUrl:'views/main.html',controllerAs:'main',controller:'MainCtrl'resolve:{doSomething:function(){returnSomeService.doSomething();}}});}// recommendedfunctionMainCtrl(SomeService){this.something=SomeService.something;}MainCtrl.resolve={doSomething:function(SomeService){returnSomeService.doSomething();}};functionconfig($routeProvider){$routeProvider.when('/',{templateUrl:'views/main.html',controllerAs:'main',controller:'MainCtrl'resolve:MainCtrl.resolve});}
こうすることで controller と同じファイル内に resolve の依存を持たせ、router をロジックから開放する
$scope: scope 間をつなぐイベントトリガーとして
$emitと$broadcastを使う// up the $scope$scope.$emit('customEvent',data);// down the $scope$scope.$broadcast('customEvent',data);
$rootScope: アプリケーション全体のイベントとして
$emitを使い、忘れずにリスナーをアンバインドする// all $rootScope.$on listeners$rootScope.$emit('customEvent',data);
ヒント:
$rootScope.$onリスナーは、$scope.$onリスナーと異なり常に残存するため、関連する$scopeが$destroyイベントを発生させたときに破棄する必要がある// call the closurevarunbind=$rootScope.$on('customEvent'[,callback]);$scope.$on('$destroy',unbind);
$rootScopeリスナーが複数ある場合は、Object リテラルでループして$destroyイベント時に自動的にアンバインドさせるvarrootListeners={'customEvent1':$rootScope.$on('customEvent1'[,callback]),'customEvent2':$rootScope.$on('customEvent2'[,callback]),'customEvent3':$rootScope.$on('customEvent3'[,callback])};for(varunbindinrootListeners){$scope.$on('$destroy',rootListeners[unbind]);}
ワンタイムバインド: Angular の新しいバージョン(v1.3.0-beta.10+)では、ワンタイムバインドのシンタックス
{{ ::value }}を利用する// avoid<h1>{{ vm.title }}</h1>// recommended<h1>{{ ::vm.title }}</h1>
Why? :
undefinedの変数が解決されたときに$$watchersから取り除き、ダーティチェックでのパフォーマンスを改善する$scope.$digest を検討:
$scope.$applyでなく$scope.$digestを使い、子スコープのみを更新する$scope.$digest();
Why? :
$scope.$applyは$rootScope.$digestを呼び出すため、アプリケーション全体の$$watchersをダーティチェックするが、$scope.$digestは$scopeのスコープと子スコープを更新する
$document と $window:
$documentと$windowを常に利用する// avoidfunctiondragUpload(){return{link:function($scope,$element,$attrs){document.addEventListener('click',function(){});}};}// recommendedfunctiondragUpload(){return{link:function($scope,$element,$attrs,$document){$document.addEventListener('click',function(){});}};}
$timeout と $interval: Angular の双方向データバインドが最新の状態を維持するよう
$timeoutと$intervalを利用する// avoidfunctiondragUpload(){return{link:function($scope,$element,$attrs){setTimeout(function(){//},1000);}};}// recommendedfunctiondragUpload($timeout){return{link:function($scope,$element,$attrs){$timeout(function(){//},1000);}};}
jsDoc: jsDoc で function 名、説明、パラメータ、返り値をドキュメント化する
/** *@name SomeService *@desc Main application Controller */functionSomeService(SomeService){/** *@name doSomething *@desc Does something awesome *@param {Number} x First number to do something with *@param {Number} y Second number to do something with *@returns {Number} */this.doSomething=function(x,y){returnx*y;};}angular.module('app').service('SomeService',SomeService);
ng-annotate:
ng-minは deprecated なので、ng-annotate for Gulp を利用し、/** @ngInject */で function にコメントして自動的に DI (dependency injection) させる/** *@ngInject */functionMainCtrl(SomeService){this.doSomething=SomeService.doSomething;}angular.module('app').controller('MainCtrl',MainCtrl);
以下のような
$injectアノテーションを含む出力となる/** *@ngInject */functionMainCtrl(SomeService){this.doSomething=SomeService.doSomething;}MainCtrl.$inject=['SomeService'];angular.module('app').controller('MainCtrl',MainCtrl);
その他、API リファレンスなどの情報は、Angular documentation を確認する。
Open an issue first to discuss potential changes/additions.
Copyright (c) 2014 Todd Motto
Permission is hereby granted, free of charge, to any person obtaininga copy of this software and associated documentation files (the'Software'), to deal in the Software without restriction, includingwithout limitation the rights to use, copy, modify, merge, publish,distribute, sublicense, and/or sell copies of the Software, and topermit persons to whom the Software is furnished to do so, subject tothe following conditions:
The above copyright notice and this permission notice shall beincluded in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OFMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANYCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THESOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
About
AngularJS styleguide for teams
Resources
Uh oh!
There was an error while loading.Please reload this page.