トピック
Angular2 CORE DOCUMENTATIONのGUIDEの翻訳です。
- DOCUMENTATION OVERVIEW - ドキュメント概要 -
- ARCHITECTURE OVERVIEW - 構文概要 -
- DISPLAYING DATA - データ表示 -
- USER INPUT - ユーザー入力 -
- FORMS - フォーム -
- DEPENDENCY INJECTION - 依存性注入 -
- STYLE GUIDE - スタイルガイド -
注意1)ここに掲載されていない項目は、Angular2 CORE DOCUMENTATIONのGUIDEを直接参照してください。
注意2)2016年12月12日時点の翻訳です。翻訳者はTOEICで700点くらいの英語力なので、英訳が間違っている可能性があります。しかもかなり意訳している箇所もあります。もし意訳を通り越して、誤訳になっているような箇所がありましたらご指摘ください。
STYLE GUIDE - スタイルガイド -
Angularをスタイリッシュに書いていきましょう。
Angularスタイルガイドへようこそ
目的
Angular構文、表記法、およびアプリケーション構造に関する有益なガイドをお探しですか?それならここにあります!このスタイルガイドでは、好ましい書き方を提示し、それが重要となる理由を説明しています。
スタイルの用語解説
それぞれのガイドラインは、すべて一貫した体裁を持ったうえで、よい実践例と悪い実践例を提示しています。
各ガイドラインの文頭は、その推奨度合の強さを示しています。
原則は常に従うべきものです。文字通り必ず従わなければならないガイドラインは非常にまれなので、「常に」は少し言葉が強すぎるかもしれません。とはいえ、原則となっているガイドラインに従わなくてもよいようなケースはかなり限定的になります。
検討と書かれたガイドラインは、基本的にガイドラインを遵守することを検討してください。 ガイドラインの背後にある意味を十分に理解し、逸脱する正当な理由がある場合は、そうしてください。 一貫性を保てるよう努めることが大切です。
回避は基本的に行うべきではないことを示しています。 回避に使うコード例には、必ず赤いヘッダーがあります。
※訳注)原文ママ。このページはマークダウン形式の都合上、回避用のコード例に赤いヘッダーはありません。
ファイルの構成ルール
いくつかのコード例では、1つ以上、同じ名前の付随ファイルを持ったファイルを示しています (例:hero.component.tsとhero.component.html)。
ガイドラインは、これらのさまざまなファイルを表現するために、hero.component.ts | html | css | spec
というショートカットを使用します。 このショートカットを利用すると、このガイドのファイル構造が読みやすくなり、簡潔になります。
目次
単一責任
単一責任の原則をすべてのコンポーネント、サービス、およびその他のシンボルに適用します。これによってアプリをよりきれいで、読みやすく保守しやすい、テストが可能なものにしやすくなります。
1つのルール
原則 ファイルごとに1つのもの(サービスやコンポーネントなど)を定義します。
検討 1ファイルあたり400行のコードに制限します。
理由 ファイルごとに1つのコンポーネントを使用することで、可読性、保守性を向上させ、チーム内でのソース管理時における衝突を回避します。
理由 ファイルごとに1つのコンポーネントにしておけば、同一ファイル内でコンポーネントを結合させるときに生じることがある変数の共有や、不要なクロージャの生成、依存性の結合といった隠れたバグを回避することができます。
理由 単一のコンポーネントを、そのファイルのデフォルトエクスポートにすることができます。これにより、ルータでの遅延読み込みがしやすくなります。
大切なことは、コードをより再利用しやすく、読みやすく、間違いが少ないものにすることです。
次の回避例では、 AppComponent
の定義、アプリケーションの起動、Heroモデルオブジェクトの定義、サーバーからのヒーローの呼び出しを、すべて同一ファイルでやっています。 こうはしないでください。
/* avoid */
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { BrowserModule } from '@angular/platform-browser';
import { NgModule, Component, OnInit } from '@angular/core';
class Hero {
id: number;
name: string;
}
@Component({
selector: 'my-app',
template: `
<h1>{{title}}</h1>
<pre>{{heroes | json}}</pre>
`,
styleUrls: ['app/app.component.css']
})
class AppComponent implements OnInit {
title = 'Tour of Heroes';
heroes: Hero[] = [];
ngOnInit() {
getHeroes().then(heroes => this.heroes = heroes);
}
}
@NgModule({
imports: [ BrowserModule ],
declarations: [ AppComponent ],
exports: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
platformBrowserDynamic().bootstrapModule(AppModule);
const HEROES: Hero[] = [
{id: 1, name: 'Bombasto'},
{id: 2, name: 'Tornado'},
{id: 3, name: 'Magneta'},
];
function getHeroes(): Promise<Hero[]> {
return Promise.resolve(HEROES); // TODO: get hero data from the server;
}
コンポーネントと振舞いのサポートは、それぞれの従属ファイルで再定義した方がよいでしょう。
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule);
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { HeroesComponent } from './heroes/heroes.component';
@NgModule({
imports: [
BrowserModule,
],
declarations: [
AppComponent,
HeroesComponent
],
exports: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
import { Component } from '@angular/core';
import { HeroService } from './heroes';
@Component({
moduleId: module.id,
selector: 'toh-app',
template: `
<toh-heroes></toh-heroes>
`,
styleUrls: ['app.component.css'],
providers: [ HeroService ]
})
export class AppComponent { }
import { Component, OnInit } from '@angular/core';
import { Hero, HeroService } from './shared';
@Component({
selector: 'toh-heroes',
template: `
<pre>{{heroes | json}}</pre>
`
})
export class HeroesComponent implements OnInit {
heroes: Hero[] = [];
constructor(private heroService: HeroService) {}
ngOnInit() {
this.heroService.getHeroes()
.then(heroes => this.heroes = heroes);
}
}
import { Injectable } from '@angular/core';
import { HEROES } from './mock-heroes';
@Injectable()
export class HeroService {
getHeroes() {
return Promise.resolve(HEROES);
}
}
export class Hero {
id: number;
name: string;
}
import { Hero } from './hero.model';
export const HEROES: Hero[] = [
{id: 1, name: 'Bombasto'},
{id: 2, name: 'Tornado'},
{id: 3, name: 'Magneta'},
];
アプリが大きくなるにつれて、このルールはさらに重要になります。
スモールファンクション
原則 関数は小さく定義します。
検討 75行以下に制限します。
理由 関数を小さくすると、テストが容易になります(特に1つのアクションで1つの目的を果たすような場合)。
理由 関数を小さくすると、再利用がしやすくなります。
理由 関数を小さくすると、可読性が向上します。
理由 関数を小さくすると、保守性が向上します。
理由 関数を小さくすれば、大きな関数で生じやすい外部スコープとの変数共有や、不要なクロージャの生成、不要な依存性の結合といった隠れたバグを回避できます。
命名
命名規則は、保守性と可読性にとって非常に重要です。 このガイドでは、ファイル名とシンボル名による命名規則を推奨しています。
普遍的な命名ガイドライン
原則 すべてのシンボルに一貫した名前を使用します。
原則 シンボルを特徴、タイプの順に記述するパターンに従います。 推奨されるパターンの例は、
feature.type.ts
です。
理由 命名規則によって一貫性を持つことで、コンテンツを一目で見つけることができるようになります。 プロジェクト内の一貫性は必須で、チーム内での一貫性は重要です。 企業全体で一貫性を保つことができれば、非常に効率的になります。
理由 命名規則を使えば、目的のコードをより速く見つけ、理解しやすくすることができます。
理由 フォルダとファイルの名前は、その意図を明確に伝える必要があります。 例えば
app / heroes / hero-list.component.ts
であれば、ヒーローのリストを管理するコンポーネントが含まれている可能性があります。
ドットとダッシュでファイル名を区切る
原則 説明的な名称を単語で区切るにはダッシュを使用します。
原則 ドットを使用して、説明的な名称とそのタイプを区切ります。.
原則 コンポーネントの機能、タイプの順に記述するパターンに従うならば、すべてのコンポーネントに対して一貫したタイプの名称を使用します。 推奨するパターンの例は
feature.type.ts
です。
原則
.service
、.component
、.pipe
、.module
、.directive
を含む従来のタイプ名を使用してください。とはいえ、必要があれば作りすぎに注意しつつ、追加のタイプ名を作成してください。
理由 タイプ名は、ファイルの内容をすばやく識別するできるようになる一貫性を提供してくれます。
理由 エディタやIDEにあるファジー検索の技術を駆使して、特定のファイルタイプを簡単に見つけることができます。
理由
.service
のような省略されていないタイプ名は説明的であり、はっきりしています。.srv
、.svc
、.serv
のような略語だと混乱してしまうかもしれません。
理由 自動化したタスクで、パターンマッチングをしてくれるようになります。
シンボルとファイル名
原則 その性質にちなんで命名されたすべてのアセットに対して、一貫した名称を使用します。
原則 クラス名にはアッパーキャメルケースを使用してください。 シンボルの名前とファイルの名前を一致させます。
原則 シンボルのタイプ(例:
Component
、Directive
、Module
、Pipe
、Service
)を通常の接尾辞として付けたシンボル名を追加してください。
原則 ファイルタイプに通常の接尾辞をつけたもの(例:
.component.ts
、.directive.ts
、.module.ts
、.pipe.ts
、.service.ts
)をファイル名に加えます。
理由 一貫性によって、アセットを素早く識別し、参照できるようになります。
理由 アッパーキャメルケースは、コンストラクタを使用してインスタンス化できるオブジェクトを識別するために昔から使われているものです。
@Component({ ... })
export class AppComponent { }
@Component({ ... })
export class HeroesComponent { }
@Component({ ... })
export class HeroListComponent { }
@Component({ ... })
export class HeroDetailComponent { }
@Directive({ ... })
export class ValidationDirective { }
@NgModule({ ... })
export class AppModule
@Pipe({ name: 'initCaps' })
export class InitCapsPipe implements PipeTransform { }
@Injectable()
export class UserProfileService { }
サービス名
原則 その性質にちなんで命名されたすべてのサービスに対して、一貫した名称を使用します。
原則 サービスにはアッパーキャメルケースを使用します。
原則 その存在が不明瞭なとき(例えば、名詞のとき)、サービスには
Service
を接尾辞として加えます。
理由 一貫性によって、サービスを素早く識別し、参照することができるようになります。
理由
Logger
のような役割が明確なサービス名は、接尾辞を必要としません。
理由
Credit
のようなサービス名は名詞であり、接尾辞を必要とします。それがサービスなのか他のものなのか明らかでないときは、接尾辞を付けるべきです。
@Injectable()
export class HeroDataService { }
@Injectable()
export class CreditService { }
@Injectable()
export class Logger { }
ブートストラップ(起動)
原則 アプリケーションのブートストラップとプラットフォームロジックを
main.ts
という名前のファイルに入れてください。
原則 ブートストラップのロジックに、エラー処理を含めます。
回避 アプリケーションロジックを
main.ts
には入れないでください。コンポーネント、またはサービスに配置することを検討してください。
理由 アプリケーションの起動ロジックに関する共通規約に従います。
理由 他のテクノロジープラットフォームの慣習に従います。
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule)
.then(success => console.log(`Bootstrap success`))
.catch(err => console.error(err));
ディレクティブセレクタ
原則 ディレクティブのセレクタに名前を付けるときは、ローワーキャメルケースを使用してください。
理由 ビューにバインドされたディレクティブで定義され、属性名と一致するプロパティの名前を保持します。
理由 AngularのHTMLパーサーは大文字と小文字を区別し、ローワーキャメルケースを認識します。
コンポーネントのカスタム接頭辞
原則 ハイフンで区切られた小文字の要素セレクタ値(例:
admin-users
)を使用してください。
原則 コンポーネントセレクタのカスタムプレフィックスを使用します。 たとえば、接頭辞
toh
はTour of Heroesを表し、接頭辞adminはadmin
機能領域を表します。
原則 機能領域またはアプリ自体を識別するプレフィックスを使用します。
理由 他のアプリケーションやネイティブHTML要素のコンポーネントとの、要素名の衝突を防ぎます。
理由 他のアプリにあるコンポーネントを共有したり、助長しやすくします。
理由 コンポーネントはDOMで簡単に識別できます。
/* avoid */
// HeroComponent is in the Tour of Heroes feature
@Component({
selector: 'hero'
})
export class HeroComponent {}
/* avoid */
// UsersComponent is in an Admin feature
@Component({
selector: 'users'
})
export class UsersComponent {}
@Component({
selector: 'toh-hero'
})
export class HeroComponent {}
@Component({
selector: 'admin-users'
})
export class UsersComponent {}
ディレクティブのカスタム接頭辞
原則 ディレクティブのセレクタにカスタムプレフィックス(例:Tour of Heroesの接頭辞
toh
)を使用します。
原則 セレクタがネイティブのHTML属性と一致する場合を除いて、ローワーキャメルケースで非要素セレクタを綴ります。
理由 名前の衝突を防ぎます。
理由 ディレクティブが簡単に識別できます。
/* avoid */
@Directive({
selector: '[validate]'
})
export class ValidateDirective {}
@Directive({
selector: '[tohValidate]'
})
export class ValidateDirective {}
パイプ名
原則 その性質にちなんで命名されたすべてのパイプに、一貫した名称を使用します。
理由 一貫性によって、パイプを素早く識別し、参照することができるようになります。
@Pipe({ name: 'ellipsis' })
export class EllipsisPipe implements PipeTransform { }
@Pipe({ name: 'initCaps' })
export class InitCapsPipe implements PipeTransform { }
単体テストファイル名
原則 テスト用のファイルは、テストするコンポーネントと同じ名前にします。
原則 テスト用ファイルは、接尾辞
.spec
をつけた名前にします。
理由 一貫性によって、テストが素早く識別できるようになります。
理由 karmaまたは他のテストランナーのパターンマッチングを提供します。
シンボル名 | ファイル名 |
---|---|
Components | heroes.component.spec.ts hero-list.component.spec.ts hero-detail.component.spec.ts |
Services | logger.service.spec.ts hero.service.spec.ts filter-text.service.spec.ts |
Pipes | ellipsis.pipe.spec.ts init-caps.pipe.spec.ts |
e2eテストファイル名
原則 テストする機能にちなんでエンドツーエンドのテスト用ファイルを命名するときは、接尾辞
.e2e-spec
をつけます。
理由 一貫性によって、エンドツーエンドのテストが素早く識別できるようになります。
理由 テストランナーとビルド自動化のパターンマッチングを提供します。
シンボル名 | ファイル名 |
---|---|
End to End Tests | app.e2e-spec.ts, heroes.e2e-spec.ts |
AngularのNgModule名
原則 シンボル名に接尾辞Moduleを付けます。
原則 ファイル名に
.module.ts
拡張子を付けます。
原則 モジュールが存在するフォルダーと機能にちなんで、モジュールの名前を付けます。
理由 一貫性によって、モジュールを素早く識別し、参照することができるようになります。
理由 アッパーキャメルケースは、コンストラクタを使用してインスタンス化できるオブジェクトを識別するために昔から使われているものです。
理由 同じ名前の機能をルートとして、モジュールを簡単に識別できます。
原則 RoutingModuleクラス名には、RoutingModuleという接尾辞を付けます。
原則 RoutingModuleのファイル名は、
-routing.module.ts
で終了します。
理由 RoutingModuleは、Angularルーターの設定専用モジュールです。クラス名とファイル名に共通の命名規則を使うことで、これらのモジュールを簡単に見つけて検証することができます。
@NgModule({ ... })
export class AppModule { }
@NgModule({ ... })
export class HeroesModule { }
@NgModule({ ... })
export class VillainsModule { }
@NgModule({ ... })
export class AppRoutingModule { }
@NgModule({ ... })
export class HeroesRoutingModule { }
コーディング規約
コーディング、命名、および空白の規則には、一貫性をもたせてください。
クラス
原則 クラスに名前を付けるときは、アッパーキャメルケースを使用します。
理由 従来のクラス名の考え方に従います。
理由 クラスをインスタンス化して、インスタンスを構築することができます。 規約により、アッパーキャメルケースはインスタンス化が可能なアセットを示します。
/* avoid */
export class exceptionService {
constructor() { }
}
export class ExceptionService {
constructor() { }
}
定数
原則 アプリケーション動作中に値が変更されるべきでないような場合、変数を
const
で宣言してください。
理由 値が不変であることを、コードを読む者に伝えます。
理由 TypeScriptでは、即時初期化を要求し、その後の再割り当てを防ぐことで、その意図を強化することができます。
検討
const
変数は、ローワーキャメルケースで綴ります。
理由 ローワーキャメルケースの変数名(
heroRoutes
)は、従来のUPPER_SNAKE_CASE名(HERO_ROUTES
)よりも読みやすく理解しやすいです。
理由 UPPER_SNAKE_CASEを使って定数を名づける従来のやり方は、
const
宣言をすばやく判別することのできる現代IDE以前の時代を反映しています。 TypeScript自体は、偶発的な再割り当てをさせません。
原則 UPPER_SNAKE_CASEで綴られた既存の
const
変数は許容されます。
理由 UPPER_SNAKE_CASEという古い手法は、特にサードパーティ製のモジュールで広く普及しています。 それらを変更するために努力したり、既存のコードや文書を破壊するリスクをとるほどの価値はほとんどありません。
export const mockHeroes = ['Sam', 'Jill']; // prefer
export const heroesUrl = 'api/heroes'; // prefer
export const VILLAINS_URL = 'api/villains'; // tolerate
インターフェース
原則 アッパーキャメルケースを使用して、インターフェースに名前を付けます。
検討
I
プリフィックスを付けずにインタフェース名を指定します。
検討 インタフェースの代わりにクラスを使用します。
理由 TypeScriptガイドラインでは、"I" プリフィックスを奨励していません。
理由 クラスだけにすれば、クラスプラスインターフェイスよりもコードが少なくなります。
理由 クラスはインタフェースとしても動作できます(
extends
の代わりにimplements
を使用してください)。
理由 interface-classは、AngularDIのプロバイダルックアップトークンにすることができます。
/* avoid */
import { Injectable } from '@angular/core';
import { IHero } from './hero.model.avoid';
@Injectable()
export class HeroCollectorService {
hero: IHero;
constructor() { }
}
import { Injectable } from '@angular/core';
import { Hero } from './hero.model';
@Injectable()
export class HeroCollectorService {
hero: Hero;
constructor() { }
}
プロパティとメソッド
原則 ローワーキャメルケースを使用して、プロパティとメソッドの名前を付けます。
回避 プライベートなプロパティとメソッドの接頭辞に、アンダースコアを付ないでください。
理由 プロパティとメソッドの従来の考え方に従います。
理由 JavaScript自体にプライベートなプロパティやメソッドはありません。
理由 TypeScriptツールを使用すると、プライベートとパブリックのプロパティ、メソッドを簡単に識別できます。
/* avoid */
import { Injectable } from '@angular/core';
@Injectable()
export class ToastService {
message: string;
private _toastCount: number;
hide() {
this._toastCount--;
this._log();
}
show() {
this._toastCount++;
this._log();
}
private _log() {
console.log(this.message);
}
}
import { Injectable } from '@angular/core';
@Injectable()
export class ToastService {
message: string;
private toastCount: number;
hide() {
this.toastCount--;
this.log();
}
show() {
this.toastCount++;
this.log();
}
private log() {
console.log(this.message);
}
}
インポートの行間
検討 サードパーティのインポートとアプリケーションのインポートの間に、空行を1つ残します。
検討 モジュール単位で、アルファベット順にインポート行を並べます。
検討 バラバラにインポートされたアセットをアルファベット順に並べ替えます。
理由 空行は、インポートの読み込みと配置をしやすくします。
理由 アルファベット順にすると、インポートの読み込みと配置がしやすくなります。
/* avoid */
import { ExceptionService, SpinnerService, ToastService } from '../../core';
import { Http, Response } from '@angular/http';
import { Injectable } from '@angular/core';
import { Hero } from './hero.model';
import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Hero } from './hero.model';
import { ExceptionService, SpinnerService, ToastService } from '../../core';
アプリ構成とAngularモジュール
実装を行うには、短期的なビジョンと長期的なビジョンが必要になります。もちろん最初は小規模から始めるのですが、アプリがどこに向かっているのかを考えながら実装してください。
アプリのコードはすべて、 app
という名前のフォルダに入れておきます。 すべての機能領域は、それぞれ独自のAngularモジュールを使用し、独自のフォルダへと格納します。
すべてのコンテンツは、1ファイルにつき1つのアセットとなります。コンポーネント、サービス、およびパイプは、それぞれ1つのファイルに1つずつ入れておきます。サードパーティベンダーのスクリプトは、すべて別のフォルダに保存し、appフォルダには保存しません。自分で書いていないスクリプトで、app
フォルダを散らかしたくないですよね。各ファイルには、このガイドで紹介している命名規則を利用してください。
LIFT
原則 コードを素早く見つけられること(
L
ocate)、コードを一目で識別できること(I
dentify)、最もフラットな構造を保つこと(F
lattest)、DRYになるよう試みるこ(T
ry)に配慮して、アプリを構築してください。
原則 これらの4つの基本的なガイドラインに従って構造を定義し、重要度の順に列挙します。
理由 LIFTに従えば、一貫性があってスケールがしやすく、モジュール化された、コードのみつけやすい開発者フレンドリーな構成を作ることができます。独自の構成について自分の直感を確認したいときは、*この機能の関連ファイル全てを即時に開き、作業を始められるか?*と自問自答してみてください。
Locate
原則 直感的で、簡単かつ迅速にコードを見つけられるようにします。
理由 効率的に作業しようとするなら、たとえファイルの名前を知らない(または覚えていない)ようなときでも、すばやくファイルを見つけられるようにしなければいけません。直感的にわかる場所で、かつそれと関連するファイルが近くにあれば、検索にかかる時間が節約されます。わかりやすいフォルダ構成は、あなたとあなたの後にやって来る人々に異世界を提供してくれます。
Identify
原則 そのファイルに含まれているものと、その内容がすぐわかるように命名します。
原則 ファイル名で、ファイル内に確実に1つのコンポーネントが含まれているとわかるようにしておきます。
回避 複数のコンポーネント、サービス、またはその混成を含むようなファイルは避けてください。
理由 コードの吟味にかかる時間が短縮され、効率があがります。 長いファイル名は、短くて不明瞭な省略名よりはるかに優れています。
小規模で親和性の高い機能が多く、複数のファイルにまたがるよりも1つのファイルにまとめた方が発見も理解も進むような場合、
one-thing-per-file
の法則から外れた方がよいこともあります。こういった抜道には注意してください。
Flat
原則 できるだけフラットなフォルダ構成を保ってください。
検討 フォルダに7つ以上のファイルがあるのであれば、サブフォルダを作成します。
検討 生成された
.js
ファイルや.js.map
ファイルのような無関係なファイルは隠すようにIDEを設定します。
理由 7階層もあるフォルダの中からからファイルを検索したいとは誰も思いません。フラットな構造であれば、簡単にスキャンできます。
一方心理学では、面白そうなものが10以上並んでいると悩み始めるとされています。なので1つのフォルダに10個以上のファイルがある場合は、サブフォルダを作成する必要があります。
あなたが快適だと思う階層になることを心掛けて決定してください。新しいフォルダを作成するに足る明白な価値が生じるまでは、フラットな構造を使用しましょう。
T-DRY (DRYになるよう試みる)
原則 DRYにしてください。 (同じものを繰り返さないでください。)
回避 可読性を犠牲にしてまで、DRYにする必要はありません。
理由 DRYは重要ですが、LIFTの他の要素を犠牲にするほどではありません。 それが、T-DRYと呼ばれる由縁です。 たとえば、コンポーネントが明示的にビューであるため、コンポーネントの名前を
hero-view.component.html
とするのは冗長です。 しかし、わかりづらいものがある、または慣例から外れているような場合は、それを書き出すようにしてください。
全体的な構成のガイドライン
原則 小規模から始めたとしても、アプリがどこに向かっているのかを考えながら実装してください。
原則 短期的なビジョンと長期的なビジョンを持って実装してください。
原則 すべてのアプリケーションのコードを
app
という名前のフォルダに入れてください。
検討 複数の付随ファイル(
.ts
、.html
、.css
、.spec
)がある場合、コンポーネント用のフォルダを作成してください。
理由 初期段階で維持しやすくなるようアプリの構成を小さくかつ簡潔にしつつも、アプリの成長に合わせて改修しやすくなるようにしておきます。
理由 コンポーネントに4種類のファイル(例:
*.html
、*.css
、*.ts
、*.spec.ts
)があると、フォルダの中がすぐに乱雑になります。
以上に準拠したフォルダ、ファイルの構成は次のようになります。
<project root>
├app
│ ├core
│ │ ├core.module.ts
│ │ ├exception.service.ts|spec.ts
│ │ └user-profile.service.ts|spec.ts
│ ├heroes
│ │ ├hero
│ │ │ └hero.component.ts|html|css|spec.ts
│ │ ├hero-list
│ │ │ └hero-list.component.ts|html|css|spec.ts
│ │ ├shared
│ │ │ ├hero-button.component.ts|html|css|spec.ts
│ │ │ ├hero.model.ts
│ │ │ └hero.service.ts|spec.ts
│ │ ├heroes.component.ts|html|css|spec.ts
│ │ ├heroes.module.ts
│ │ └heroes-routing.module.ts
│ ├shared
│ │ ├shared.module.ts
│ │ ├init-caps.pipe.ts|spec.ts
│ │ ├text-filter.component.ts|spec.ts
│ │ └text-filter.service.ts|spec.ts
│ ├villains
│ │ ├villain
│ │ │ └...
│ │ ├villain-list
│ │ │ └...
│ │ ├shared
│ │ │ └...
│ │ ├villains.component.ts|html|css|spec.ts
│ │ ├villains.module.ts
│ │ └villains-routing.module.ts
│ ├app.component.ts|html|css|spec.ts
│ ├app.module.ts
│ └app-routing.module.ts
├main.ts
├index.html
└...
コンポーネントを専用のフォルダに入れる方法はよく使われていますが、小さなアプリではもう一つの選択肢として、コンポーネントを(専用フォルダを設けず)フラットにしておくことがあります。これにより既存のフォルダに最大で4つのファイルが追加されますが、フォルダの入れ子構造も縮小されます。どちらを選んでも、一貫性は担保されます。
機能別のフォルダ構成
原則 フォルダには、その機能領域を表現した名前をつけて作成してください。
理由 構成がフラットで、かつ反復も冗長性もない命名がされていれば、開発者はコードを見つけ、そのファイルの機能がどんなものであるかを一目で判断できるようになります。
理由 LIFTガイドラインすべてに適用されます。
理由 コンテンツを整理し、LIFTのガイドラインに準拠しておくことで、アプリが乱雑になるのを防ぎます。
理由 ファイルがたくさんある場合(10以上)、フォルダ構成に一貫性があれば目的のファイルを見つけやすくなりますが、フラットな構成では見つけるのが難しくなります。
原則 機能領域ごとにAngularモジュールを作成します。
理由 Angularモジュールは、ルーティング可能な機能の遅延読み込みをしやすくします。
理由 Angularモジュールは、機能の分離、テスト、再利用をしやすくします。
このフォルダとファイルの構成は、この例を参照してください。
Appルートモジュール
原則 アプリのルートフォルダにAngularモジュールを作成します(例:
/app
の中)。
理由 すべてのアプリには、少なくとも1つルートAngularモジュールが必要です。
検討 ルートモジュールは
app.module.ts
という名前にします。
理由 ルートモジュールの特定と識別が容易になります。
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { HeroesComponent } from './heroes/heroes.component';
@NgModule({
imports: [
BrowserModule,
],
declarations: [
AppComponent,
HeroesComponent
],
exports: [ AppComponent ],
entryComponents: [ AppComponent ]
})
export class AppModule {}
機能モジュール
原則 アプリにあるすべての異なる機能(例:
Heroes
機能)に対してAngularモジュールを作成します。
原則 機能モジュールを、機能領域と同じ名前のフォルダに配置します(例:
app/heroes
)。
原則 機能領域とフォルダの名前を反映して、機能モジュールファイルの名前を付けます(例:
app/heroes/heroes.module.ts
)。
原則 機能領域、フォルダ、ファイルの名前を反映して、機能モジュールのシンボルに名前を付けます(例:
app/heroes/heroes.module.ts
であれば、HeroesModule
)。
理由 機能モジュールは、他のモジュールに対して実装を公開、非公開にすることができます。
理由 機能モジュールは、機能領域を構成する関連コンポーネントのセットを識別します。
理由 機能モジュールは、一括、遅延のどちらでも簡単にルーティングできます。
理由 機能モジュールは、特定の機能と他のアプリケーション機能との明確な境界を定義します。
理由 機能モジュールは、開発チーム間の責任を明確にしやすくします。
理由 機能モジュールは、テスト用の分離をしやすくします。
共有機能モジュール
原則
shared
フォルダにSharedModule
という名前の機能モジュールを作成します(例:app/shared/shared.module.ts
であればSharedModule
)。
原則
SharedModule
内の他の機能モジュールによってアプリケーション全体で使用される共通コンポーネント、ディレクティブおよびパイプを配置します。これらのアセットでは、シングルトンではない新しいインスタンスを共有することが期待されます。
原則
SharedModule
でアセットに必要なすべてのモジュールをインポートします(例:CommonModule
、FormsModule
)。
理由
SharedModule
には、他の共通モジュールにある機能が必要なコンポーネント、ディレクティブ、パイプが含まれています(例:CommonModule
のngFor
)。
原則
SharedModule
内のすべてのコンポーネント、ディレクティブ、およびパイプを宣言します。
原則 他の機能モジュールから使用を求められる
SharedModule
のシンボルすべてをエクスポートします。
理由
SharedModule
は、共通のコンポーネント、ディレクティブおよびパイプを作成し、その他多くのモジュールにあるコンポーネントのテンプレートで利用できるようにします。
回避
SharedModule
にアプリ全体にかかるシングルトンプロバイダを指定しないでください。意図的なシングルトンならOKですので、注意してください。
理由 遅延読み込みで共有モジュールをインポートした機能モジュールは、サービス自身のコピーを作成し、望ましくない結果をもたらす可能性があります。
理由 それぞれのモジュールが、それぞれ独自に分離したシングルトンサービスのインスタンスを持つことは望ましくありません。 しかし、
SharedModule
がサービスを提供すると、それが実際に生じてしまう危険性があります。
src
├app
│ ├shared
│ │ ├shared.module.ts
│ │ ├init-caps.pipe.ts|spec.ts
│ │ ├text-filter.component.ts|spec.ts
│ │ └text-filter.service.ts|spec.ts
│ ├app.component.ts|html|css|spec.ts
│ ├app.module.ts
│ └app-routing.module.ts
├main.ts
├index.html
└...
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { FilterTextComponent } from './filter-text/filter-text.component';
import { FilterTextService } from './filter-text/filter-text.service';
import { InitCapsPipe } from './init-caps.pipe';
@NgModule({
imports: [CommonModule, FormsModule],
declarations: [
FilterTextComponent,
InitCapsPipe
],
providers: [FilterTextService],
exports: [
CommonModule,
FormsModule,
FilterTextComponent,
InitCapsPipe
]
})
export class SharedModule { }
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'initCaps' })
export class InitCapsPipe implements PipeTransform {
transform = (value: string) => value;
}
import { Component, EventEmitter, Output } from '@angular/core';
@Component({
moduleId: module.id,
selector: 'toh-filter-text',
template: '<input type="text" id="filterText" [(ngModel)]="filter" (keyup)="filterChanged($event)" />'
})
export class FilterTextComponent {
@Output() changed: EventEmitter<string>;
filter: string;
constructor() {
this.changed = new EventEmitter<string>();
}
clear() {
this.filter = '';
}
filterChanged(event: any) {
event.preventDefault();
console.log(`Filter Changed: ${this.filter}`);
this.changed.emit(this.filter);
}
}
import { Injectable } from '@angular/core';
@Injectable()
export class FilterTextService {
constructor() {
console.log('Created an instance of FilterTextService');
}
filter(data: string, props: Array<string>, originalList: Array<any>) {
let filteredList: any[];
if (data && props && originalList) {
data = data.toLowerCase();
let filtered = originalList.filter(item => {
let match = false;
for (let prop of props) {
if (item[prop].toString().toLowerCase().indexOf(data) > -1) {
match = true;
break;
}
};
return match;
});
filteredList = filtered;
} else {
filteredList = originalList;
}
return filteredList;
}
}
import { Component, OnInit } from '@angular/core';
import { FilterTextService } from '../shared/filter-text/filter-text.service';
@Component({
moduleId: module.id,
selector: 'toh-heroes',
templateUrl: 'heroes.component.html'
})
export class HeroesComponent implements OnInit {
filteredHeroes: any[] = [];
constructor(private filterService: FilterTextService) { }
heroes = [
{ id: 1, name: 'Windstorm' },
{ id: 2, name: 'Bombasto' },
{ id: 3, name: 'Magneta' },
{ id: 4, name: 'Tornado' }
];
filterChanged(searchText: string) {
this.filteredHeroes = this.filterService.filter(searchText, ['id', 'name'], this.heroes);
}
ngOnInit() {
this.filteredHeroes = this.heroes;
}
}
<div>This is heroes component</div>
<ul>
<li *ngFor="let hero of filteredHeroes">
{{hero.name}}
</li>
</ul>
<toh-filter-text (changed)="filterChanged($event)"></toh-filter-text>
コア機能モジュール
原則 単一利用のクラスを集めて、CoreModuleの中にその見せたくない詳細部分を隠します。 簡略化されたルートの
AppModule
は、アプリケーション全体のオーケストレーターとしての能力でCoreModule
をインポートします。
原則 コアフォルダに
CoreModule
という名前の機能モジュールを作成します(例:app/core/core.module.ts
であれば、CoreModule
)
原則 インスタンスがアプリケーション全体で共有されるシングルトンサービスを、
Core Module
に配置します(例:ExceptionService
、LoggerService
)。
原則
CoreModule
のアセットで要求されるモジュールをすべてインポートします(例:CommonModule
、FormsModule
)。
理由
CoreModule
は、1つ以上のシングルトンサービスを提供します。 Angularはプロバイダにアプリのルートインジェクタを登録し、一括、もしくは遅延して読み込まれたコンポーネントが各サービスのシングルトンインスタンスを利用できるようにします。
理由
CoreModule
はシングルトンサービスを含みます。 遅延読み込みされたモジュールがこれらをインポートすると、意図されたアプリケーション全体のシングルトンではなく、新しいインスタンスが取得されます。
原則 CoreModuleに、アプリケーション全体で1回しか使用しないコンポーネントを集めます。 アプリケーションを起動するときに一度(
AppModule
の中で)それをインポートして、他のどこからもインポートしないでください(例:NavComponent
、SpinnerComponent
)。
理由 現実世界のアプリケーションでは、
AppComponent
テンプレートにのみ現れる複数の使い捨てコンポーネント(例えばスピナー、メッセージトースト、モーダルダイアログ)を持つことができます。 それらは他の場所ではインポートされないので、その意味では共有されるというわけではありませんが、大きくて面倒なものをルートフォルダに残したままにはしておけません。
回避
CoreModule
をAppModule
以外のところからインポートしないでください。
理由 遅延読み込みで
CoreModule
を直接インポートした機能モジュールは、サービス自身のコピーを作成し、望ましくない結果をもたらす可能性があります。
理由 一括読み込みをされた時点で、機能モジュールは
AppModule
のインジェクタ、つまりCoreModule
のサービスにアクセスしています。
原則
AppModule
でCoreModule
をインポートし、他の機能モジュールからも利用できるように、CoreModule
にあるシンボルをすべてエクスポートします。
理由*
CoreModule
は、よく使われるシングルトンサービスを他の多くのモジュールでも利用できるようにするために存在します。
理由 アプリケーション全体で1つのシングルトンインスタンスを使用することが望ましく、それぞれのモジュールがそれぞれ分離したシングルトンサービスのインスタンスを持つことは望ましくありません。 しかし、
CoreModule
がサービスを提供すると、誤ってそれが実際に生じてしまう危険性があります。
src
├app
│ ├core
│ │ ├core.module.ts
│ │ ├logger.service.ts|spec.ts
│ │ ├nav
│ │ │ └nav.component.ts|html|css|spec.ts
│ │ └spinner
│ │ ├spinner.component.ts|html|css|spec.ts
│ │ └spinner.service.ts|spec.ts
│ ├app.component.ts|html|css|spec.ts
│ ├app.module.ts
│ └app-routing.module.ts
├main.ts
├index.html
└...
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { HeroesComponent } from './heroes/heroes.component';
import { CoreModule } from './core/core.module';
@NgModule({
imports: [
BrowserModule,
CoreModule,
],
declarations: [
AppComponent,
HeroesComponent
],
exports: [ AppComponent ],
entryComponents: [ AppComponent ]
})
export class AppModule {}
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LoggerService } from './logger.service';
import { NavComponent } from './nav/nav.component';
import { SpinnerComponent } from './spinner/spinner.component';
import { SpinnerService } from './spinner/spinner.service';
@NgModule({
imports: [
CommonModule // we use ngFor
],
exports: [NavComponent, SpinnerComponent],
declarations: [NavComponent, SpinnerComponent],
providers: [LoggerService, SpinnerService]
})
export class CoreModule { }
import { Injectable } from '@angular/core';
@Injectable()
export class LoggerService {
log(msg: string) {
console.log(msg);
}
error(msg: string) {
console.error(msg);
}
}
import { Component, OnInit } from '@angular/core';
@Component({
moduleId: module.id,
selector: 'toh-nav',
templateUrl: 'nav.component.html',
styleUrls: ['nav.component.css'],
})
export class NavComponent implements OnInit {
menuItems = [
'Heroes',
'Villains',
'Other'
];
ngOnInit() { }
constructor() { }
}
<header>
<div>
<h4>Tour of Heroes</h4>
</div>
<nav>
<ul>
<li *ngFor="let item of menuItems">
{{item}}
</li>
</ul>
</nav>
<br/>
</header>
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';
import { LoggerService } from '../logger.service';
import { SpinnerState, SpinnerService } from './spinner.service';
@Component({
moduleId: module.id,
selector: 'toh-spinner',
templateUrl: 'spinner.component.html',
styleUrls: ['spinner.component.css']
})
export class SpinnerComponent implements OnDestroy, OnInit {
visible = false;
private spinnerStateChanged: Subscription;
constructor(
private loggerService: LoggerService,
private spinnerService: SpinnerService
) { }
ngOnInit() {
console.log(this.visible);
this.spinnerStateChanged = this.spinnerService.spinnerState
.subscribe((state: SpinnerState) => {
this.visible = state.show;
this.loggerService.log(`visible=${this.visible}`);
});
}
ngOnDestroy() {
this.spinnerStateChanged.unsubscribe();
}
}
<div class="spinner" [class.spinner-hidden]="!visible"> </div>
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';
export interface SpinnerState {
show: boolean;
}
@Injectable()
export class SpinnerService {
private spinnerSubject = new Subject<SpinnerState>();
spinnerState = this.spinnerSubject.asObservable();
constructor() { }
show() {
this.spinnerSubject.next(<SpinnerState>{ show: true });
}
hide() {
this.spinnerSubject.next(<SpinnerState>{ show: false });
}
}
app / rootクラスの多くが他のモジュールに移動したため、
AppModule
が少し小さくなりました。これから作られるコンポーネントとプロバイダは、このモジュールではなく、他のモジュールに加えられていくので、AppModule
は安定します。AppModule
は作業をせず、インポートした他のモジュールに仕事を渡していきます。AppModule
はアプリ全体をオーケストレーションするという、メインのタスクに集中させておきます。
コアモジュールの再インポートを防ぐ
CoreModule
をインポートするのは、ルートにあるAppModule
だけにしておくべきです。
原則 ガードロジックを追加することで
CoreModule
の再インポートを防ぎ、高速化させます。
理由
CoreModule
の再インポートを防ぎます。
理由 シングルトンを意図したアセットが、複数のインスタンスをもってしまうことを防ぎます。
export function throwIfAlreadyLoaded(parentModule: any, moduleName: string) {
if (parentModule) {
throw new Error(`${moduleName} has already been loaded. Import Core modules in the AppModule only.`);
}
}
import { NgModule, Optional, SkipSelf } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LoggerService } from './logger.service';
import { NavComponent } from './nav/nav.component';
import { throwIfAlreadyLoaded } from './module-import-guard';
@NgModule({
imports: [
CommonModule // we use ngFor
],
exports: [NavComponent],
declarations: [NavComponent],
providers: [LoggerService]
})
export class CoreModule {
constructor( @Optional() @SkipSelf() parentModule: CoreModule) {
throwIfAlreadyLoaded(parentModule, 'CoreModule');
}
}
遅延読み込みをしたフォルダ
個別のアプリケーション機能またはワークフローが、アプリケーションの起動時ではなく、遅延読み込みまたはオンデマンド読み込みされることがあります。
原則 遅延読み込みされる機能は、遅延読み込みフォルダに入れてください。遅延読み込みフォルダには、ルーティングコンポーネントとその子コンポーネント、および関連するアセットとモジュールなどが典型として含まれます。
理由 このフォルダを使用すると、機能の内容を簡単に識別して切り分けることができます。
遅延読み込みフォルダを直接インポートしない
回避 兄弟フォルダや親フォルダ内にあるモジュールが、遅延読み込み機能でモジュールを直接インポートしないようにしてください。
理由 モジュールを直接インポートして使用すると、オンデマンド読み込みを意図しているような場合であっても、すぐに読み込みが開始されます。
コンポーネント
コンポーネントセレクタの命名
原則 コンポーネントのセレクタ要素を命名するときは、ダッシュケースまたはケバブケースを使用します。
理由 要素名をカスタム要素の仕様と一致させておきます。
/* avoid */
@Component({
selector: 'tohHeroButton',
templateUrl: 'hero-button.component.html'
})
export class HeroButtonComponent {}
@Component({
selector: 'toh-hero-button',
templateUrl: 'hero-button.component.html'
})
export class HeroButtonComponent {}
<toh-hero-button></toh-hero-button>
要素としてのコンポーネント
原則 セレクタを介してコンポーネントを要素として定義します。
理由 コンポーネントには、HTMLとオプションのAngularテンプレートシンタックスを含むテンプレートがあります。 それらはページ上のコンテンツ配置と非常に関連性が高く、要素を使って並べる方がより親和性が高くなります。
理由 コンポーネントは、ページ上のビジュアル要素を表現します。 HTML要素タグとしてセレクタ定義するのは、ネイティブのHTML要素やWebComponentsと同じです。
理由 テンプレートのhtmlを見れば、シンボルがコンポーネントなのかディレクティブなのかを簡単に認識できます。
/* avoid */
@Component({
selector: '[tohHeroButton]',
templateUrl: 'hero-button.component.html'
})
export class HeroButtonComponent {}
<!-- avoid -->
<div tohHeroButton></div>
@Component({
selector: 'toh-hero-button',
templateUrl: 'hero-button.component.html'
})
export class HeroButtonComponent {}
<toh-hero-button></toh-hero-button>
自分のファイルにテンプレートとスタイルを抽出する
原則 3行以上になった場合、テンプレートとスタイルを別々のファイルに抽出します。
原則 テンプレートファイルは
[component-name].component.html
という風に名前を付けます。ここでの[component-name]はコンポーネント名です。
原則 スタイルファイルは
[component-name].component.css
という風に名前を付けます。ここでの[component-name]はコンポーネント名です。
理由 (.jsや.ts)コードファイル内にあるインラインのテンプレートだと、シンタックスヒントがサポートされていないエディタがあります。
理由 コンポーネントファイルのロジックは、テンプレートやスタイルがインラインで混ざっていない方が読みやすいです。
/* avoid */
@Component({
selector: 'toh-heroes',
template: `
<div>
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="let hero of heroes">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
<div *ngIf="selectedHero">
<h2>{{selectedHero.name | uppercase}} is my hero</h2>
</div>
</div>
`,
styleUrls: [`
.heroes {
margin: 0 0 2em 0; list-style-type: none; padding: 0; width: 15em;
}
.heroes li {
cursor: pointer;
position: relative;
left: 0;
background-color: #EEE;
margin: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 4px;
}
.heroes .badge {
display: inline-block;
font-size: small;
color: white;
padding: 0.8em 0.7em 0 0.7em;
background-color: #607D8B;
line-height: 1em;
position: relative;
left: -1px;
top: -4px;
height: 1.8em;
margin-right: .8em;
border-radius: 4px 0 0 4px;
}
`]
})
export class HeroesComponent implements OnInit {
heroes: Hero[];
selectedHero: Hero;
ngOnInit() {}
}
@Component({
selector: 'toh-heroes',
templateUrl: 'heroes.component.html',
styleUrls: ['heroes.component.css']
})
export class HeroesComponent implements OnInit {
heroes: Hero[];
selectedHero: Hero;
ngOnInit() { }
}
<div>
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="let hero of heroes">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
<div *ngIf="selectedHero">
<h2>{{selectedHero.name | uppercase}} is my hero</h2>
</div>
</div>
.heroes {
margin: 0 0 2em 0; list-style-type: none; padding: 0; width: 15em;
}
.heroes li {
cursor: pointer;
position: relative;
left: 0;
background-color: #EEE;
margin: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 4px;
}
.heroes .badge {
display: inline-block;
font-size: small;
color: white;
padding: 0.8em 0.7em 0 0.7em;
background-color: #607D8B;
line-height: 1em;
position: relative;
left: -1px;
top: -4px;
height: 1.8em;
margin-right: .8em;
border-radius: 4px 0 0 4px;
}
InputとOutputのプロパティをインラインで書く
原則
@Directive
と@Component
デコレータのinputs
とoutputs
プロパティの代わりに@Input
と@Output
を使います。
原則
@Input()
または@Output()
を、対象となるプロパティと同じ行に置きます。
理由 クラス内のどのプロパティがinputsまたはoutputsであるかを識別することで、より簡単で読みやすいものになります。
理由
@Input
または@Output
に関連付けられたプロパティやイベントの名前を変更する必要があっても、1か所の変更で済みます。
理由 ディレクティブに付随するメタデータの宣言は、より短く、より読みやすくなっています。
理由 デコレータを同じ行に配置すると、コードが短くなり、プロパティをinputまたはoutputとして簡単に識別できます。
/* avoid */
@Component({
selector: 'toh-hero-button',
template: `<button></button>`,
inputs: [
'label'
],
outputs: [
'change'
]
})
export class HeroButtonComponent {
change = new EventEmitter<any>();
label: string;
}
@Component({
selector: 'toh-hero-button',
template: `<button>{{label}}</button>`
})
export class HeroButtonComponent {
@Output() change = new EventEmitter<any>();
@Input() label: string;
}
InputsとOutputsのリネームを避ける
回避 できるだけinputsとoutputsのリネームはしないでください。
理由 指定されたディレクティブのoutputまたはinputプロパティが指定されたものの、パブリックAPIとは異なる名前でエクスポートされると、混乱することがあります。
/* avoid */
@Component({
selector: 'toh-hero-button',
template: `<button>{{label}}</button>`
})
export class HeroButtonComponent {
@Output('changeEvent') change = new EventEmitter<any>();
@Input('labelAttribute') label: string;
}
<!-- avoid -->
<toh-hero-button labelAttribute="OK" (changeEvent)="doSomething()">
</toh-hero-button>
@Component({
selector: 'toh-hero-button',
template: `<button>{{label}}</button>`
})
export class HeroButtonComponent {
@Output() change = new EventEmitter<any>();
@Input() label: string;
<toh-hero-button label="OK" (change)="doSomething()">
</toh-hero-button>
メンバの順序
原則 プロパティを上部に配置し、その下にメソッドを配置します。
原則 publicのメンバの後に、privateのメンバをアルファベット順に配置します。
理由 メンバを一貫性のある順序で並べることで、可読性が向上し、コンポーネントのどのメンバがどの目的に役立つかを即座に判断しやすくなります。
/* avoid */
export class ToastComponent implements OnInit {
private defaults = {
title: '',
message: 'May the Force be with You'
};
message: string;
title: string;
private toastElement: any;
ngOnInit() {
this.toastElement = document.getElementById('toh-toast');
}
// private methods
private hide() {
this.toastElement.style.opacity = 0;
window.setTimeout(() => this.toastElement.style.zIndex = 0, 400);
}
activate(message = this.defaults.message, title = this.defaults.title) {
this.title = title;
this.message = message;
this.show();
}
private show() {
console.log(this.message);
this.toastElement.style.opacity = 1;
this.toastElement.style.zIndex = 9999;
window.setTimeout(() => this.hide(), 2500);
}
}
export class ToastComponent implements OnInit {
// public properties
message: string;
title: string;
// private fields
private defaults = {
title: '',
message: 'May the Force be with You'
};
private toastElement: any;
// public methods
activate(message = this.defaults.message, title = this.defaults.title) {
this.title = title;
this.message = message;
this.show();
}
ngOnInit() {
this.toastElement = document.getElementById('toh-toast');
}
// private methods
private hide() {
this.toastElement.style.opacity = 0;
window.setTimeout(() => this.toastElement.style.zIndex = 0, 400);
}
private show() {
console.log(this.message);
this.toastElement.style.opacity = 1;
this.toastElement.style.zIndex = 9999;
window.setTimeout(() => this.hide(), 2500);
}
}
サービスにロジックを入れる
原則 コンポーネント内のロジックをビューに必要なロジックのみに制限します。 他のすべてのロジックはサービスに委任されるべきです。
原則 再利用可能なロジックはサービスに移行し、コンポーネントは簡潔に保ちつつ、意図した目的に集中させます。
理由 ロジックをサービス内に配置し関数を介して公開すれば、複数のコンポーネント間で再利用することができます。
理由 サービス内にロジックがあれば、ユニットテストで簡単に分離でき、コンポーネント内でロジックを呼び出せば、簡単にモックとして利用できます。
理由 依存関係を削除し、コンポーネントから実装の細部が見えないようにします。
理由 コンポーネントは、小さく整理された、その役割に集中したものにしておきます。
/* avoid */
import { OnInit } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { Hero } from '../shared/hero.model';
const heroesUrl = 'http://angular.io';
export class HeroListComponent implements OnInit {
heroes: Hero[];
constructor(private http: Http) {}
getHeroes() {
this.heroes = [];
this.http.get(heroesUrl)
.map((response: Response) => <Hero[]>response.json().data)
.catch(this.catchBadResponse)
.finally(() => this.hideSpinner())
.subscribe((heroes: Hero[]) => this.heroes = heroes);
}
ngOnInit() {
this.getHeroes();
}
private catchBadResponse(err: any, source: Observable<any>) {
// log and handle the exception
return new Observable();
}
private hideSpinner() {
// hide the spinner
}
}
import { Component, OnInit } from '@angular/core';
import { Hero, HeroService } from '../shared';
@Component({
selector: 'toh-hero-list',
template: `...`
})
export class HeroListComponent implements OnInit {
heroes: Hero[];
constructor(private heroService: HeroService) {}
getHeroes() {
this.heroes = [];
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes);
}
ngOnInit() {
this.getHeroes();
}
}
Outputプロパティに接頭辞を付けない
原則 イベントには
on
接頭辞をつけずに命名してください。
原則 イベントハンドラメソッドは、接頭辞
on
の後にイベント名が続くように命名してください。
理由 この命名規則は、ボタンクリックなどのビルトインイベントと同様です。
理由 Angularは代替構文
on-*
を許容します。イベント自体に接頭辞on
がある場合、これはon-onEvent
バインディング方式になります。
/* avoid */
@Component({
selector: 'toh-hero',
template: `...`
})
export class HeroComponent {
@Output() onSavedTheDay = new EventEmitter<boolean>();
}
<!-- avoid -->
<toh-hero (onSavedTheDay)="onSavedTheDay($event)"></toh-hero>
export class HeroComponent {
@Output() savedTheDay = new EventEmitter<boolean>();
}
<toh-hero (savedTheDay)="onSavedTheDay($event)"></toh-hero>
コンポーネントクラスにプレゼンテーションロジックを配置する
原則 プレゼンテーションロジックはコンポーネントクラスに入れ、テンプレートには入れない。
理由 ロジックは2つの場所に分散するのではなく、1つの場所(コンポーネントクラス)に集約されるようにします。
理由 テンプレートの代わりに、コンポーネントクラスにあるプレゼンテーションロジックを維持することで、テスト容易性、保守性、および再利用性が向上します。
/* avoid */
@Component({
selector: 'toh-hero-list',
template: `
<section>
Our list of heroes:
<hero-profile *ngFor="let hero of heroes" [hero]="hero">
</hero-profile>
Total powers: {{totalPowers}}<br>
Average power: {{totalPowers / heroes.length}}
</section>
`
})
export class HeroListComponent {
heroes: Hero[];
totalPowers: number;
}
@Component({
selector: 'toh-hero-list',
template: `
<section>
Our list of heroes:
<toh-hero *ngFor="let hero of heroes" [hero]="hero">
</toh-hero>
Total powers: {{totalPowers}}<br>
Average power: {{avgPower}}
</section>
`
})
export class HeroListComponent {
heroes: Hero[];
totalPowers: number;
get avgPower() {
return this.totalPowers / this.heroes.length;
}
}
ディレクティブ
ディレクティブを使用して既存要素を強化する
原則 テンプレートのないプレゼンテーションロジックを使用する場合は、属性ディレクティブを使用します。
理由 属性ディレクティブには、関連するテンプレートがありません。
理由 要素には複数の属性ディレクティブが適用されている場合があります。
@Directive({
selector: '[tohHighlight]'
})
export class HighlightDirective {
@HostListener('mouseover') onMouseEnter() {
// do highlight work
}
}
<div tohHighlight>Bombasta</div>
HostListenerおよびHostBindingクラスのデコレータを使用
検討
@HostListener
と@HostBinding
を@Directive
と@Component
デコレータのhostプロパティよりも優先させます。
原則 一貫性を持って選択してください。
理由
@HostBinding
に関連するプロパティ、または@HostListener
に関連するメソッドは、ディレクティブクラス内にある単一の場所からのみ変更できるようになります。host
メタデータのプロパティを使用する場合は、コントローラ内のプロパティ宣言と、そのディレクティブに関連するメタデータの両方を変更する必要があります。
import { Directive, HostBinding, HostListener } from '@angular/core';
@Directive({
selector: '[tohValidator]'
})
export class ValidatorDirective {
@HostBinding('attr.role') role = 'button';
@HostListener('mouseenter') onMouseEnter() {
// do work
}
}
非推奨であるhost
メタデータの代替案と比較してみてください。
理由
host
メタデータは覚えておくべき1つの用語であり、追加のESインポートを必要としません。
import { Directive } from '@angular/core';
@Directive({
selector: '[tohValidator2]',
host: {
'attr.role': 'button',
'(mouseenter)': 'onMouseEnter()'
}
})
export class Validator2Directive {
role = 'button';
onMouseEnter() {
// do work
}
}
サービス
サービスはインジェクタ内にあるシングルトン
原則 同じインジェクタ内にあるシングルトンとしてサービスを使用してください。サービスはデータと機能の共有を行う場合に使用します。
理由 サービスは、機能領域やアプリ全体でのメソッドの共有に最適です。
理由 サービスは、状態を持ったメモリ内にあるデータの共有に最適です。
export class HeroService {
constructor(private http: Http) { }
getHeroes() {
return this.http.get('api/heroes')
.map((response: Response) => <Hero[]>response.json().data);
}
}
単一責任
原則 コンテキストを元にカプセル化するという単一責任の原則に従って、サービスを作成します。
原則 サービスがその単体の目的を超え始めたとき、新しいサービスを作ります。
理由 サービスに複数の責任があると、テストするのが難しくなります。
理由 サービスに複数の責任があると、そのサービスをインジェクトするすべてのコンポーネント、またはサービスが、そのすべての重圧を受けてしまいます。
サービスの提供
原則 共有されている中で、最上位にあるコンポーネントのAngularインジェクタにサービスを提供します。
理由 Angularインジェクタは階層構造になっています。
理由 最上位にあるコンポーネントにサービスを提供する場合、そのインスタンスは共有され、その最上位コンポーネントのすべての子コンポーネントで利用できます。
理由 この手法はサービスがメソッドや状態を共有しているときに最適です。
理由 2つの異なるコンポーネントが1つのサービスの異なるインスタンスをそれぞれ必要とするような場合、この手法は理想的ではありません。このシナリオでは、新規および別個のインスタンスを必要とするコンポーネントレベルでサービスを提供する方がよいでしょう。
import { Component } from '@angular/core';
import { HeroService } from './heroes';
@Component({
selector: 'toh-app',
template: `
<toh-heroes></toh-heroes>
`,
providers: [HeroService]
})
export class AppComponent {}
import { Component, OnInit } from '@angular/core';
import { Hero, HeroService } from '../shared';
@Component({
selector: 'toh-heroes',
template: `
<pre>{{heroes | json}}</pre>
`
})
export class HeroListComponent implements OnInit {
heroes: Hero[] = [];
constructor(private heroService: HeroService) { }
ngOnInit() {
this.heroService.getHeroes().subscribe(heroes => this.heroes = heroes);
}
}
@Injectable()
クラスデコレータを使う
原則 サービスの依存関係の型をトークンとして使用する場合は、
@Inject parameter decorator
の代わりに@Injectable
クラスのデコレータを使用してください。
理由 Angular DIのメカニズムは、サービスのコンストラクタで宣言された型に基づいて、サービスのすべての依存関係を解決します。
理由 サービスが型トークンに関連付けられた依存関係のみを受け入れる場合、
@Injectable()
シンタックスは、個々のコンストラクタパラメータで@Inject()
を使用するよりもはるかに冗長になります。
/* avoid */
export class HeroArena {
constructor(
@Inject(HeroService) private heroService: HeroService,
@Inject(Http) private http: Http) {}
}
@Injectable()
export class HeroArena {
constructor(
private heroService: HeroService,
private http: Http) {}
}
データサービス
個別のデータ呼び出し
原則 データ操作、およびサービス間とのデータのやり取りを行うロジックをリファクタリングします。
原則 データサービスがXHRコール、ローカルストレージ、メモリ内スタッシュ、その他のデータ操作の役割を担います。
理由 コンポーネントの役割は、ビュー情報の表示と収集です。 どのようにしてデータを取得するのかを気にする必要はなく、誰かがデータを要求していることがわかってさえいればいいのです。データサービスを分離することで、データサービスにアクセス方法のロジックが移動し、コンポーネントがよりシンプルになってビューに集中できるようになります。
理由 これによって、データサービスを使用するコンポーネントをテストするときに、データ呼び出し(モックまたは実データ)のテストが容易になります。
理由 データサービスの実装には、データリポジトリを扱うための非常に特殊なコードが含まれている場合があります。 これには、ヘッダー、データとの対話方法、または
Http
などの他のサービスが含まれます。 ロジックをデータサービスに分離することは、このロジックを単一の場所にカプセル化して、外部のコンシューマ(おそらくコンポーネント)から実装を隠し、実装を簡単に変更できるようにします。
ライフサイクルフック
ライフサイクルフックを使用し、Angularで公開されている重要なイベントを利用します。
Implement Lifecycle Hooks Interfaces
原則 ライフサイクルフックインタフェースを実装します。
理由 強力に型付けされたメソッドシグネチャです。コンパイラとエディタは、スペルミスを喚起してくれます。
/* avoid */
@Component({
selector: 'toh-hero-button',
template: `<button>OK<button>`
})
export class HeroButtonComponent {
onInit() { // misspelled
console.log('The component is initialized');
}
}
@Component({
selector: 'toh-hero-button',
template: `<button>OK</button>`
})
export class HeroButtonComponent implements OnInit {
ngOnInit() {
console.log('The component is initialized');
}
}
付録
Angularの便利なツールとヒント
Codelyzer
原則 このガイドに従って、codelyzerを使用してください。
検討 必要に応じてコードライザーのルールを調整してください。
ファイルテンプレートとスニペット
原則 一貫性のあるスタイル、パターンに準拠しやすくするため、ファイルのテンプレートやスニペットを使用ください。 WEB開発用のエディタやIDEであれば、テンプレートやスニペットがあります。
検討 ここまで紹介してきたスタイルとガイドラインに準拠しているVisual Studio Codeにあるスニペットを使用してください。
検討 ここまで紹介してきたスタイルとガイドラインに準拠しているSublime Textにあるスニペットを使用してください。