LoginSignup
15
14

More than 5 years have passed since last update.

Angular2 DOC GUIDEを翻訳する[STYLE GUIDE]

Last updated at Posted at 2016-12-12

トピック

Angular2 CORE DOCUMENTATIONの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つのルール

STYLE 01-01

原則 ファイルごとに1つのもの(サービスやコンポーネントなど)を定義します。


検討 1ファイルあたり400行のコードに制限します。


理由 ファイルごとに1つのコンポーネントを使用することで、可読性、保守性を向上させ、チーム内でのソース管理時における衝突を回避します。


理由 ファイルごとに1つのコンポーネントにしておけば、同一ファイル内でコンポーネントを結合させるときに生じることがある変数の共有や、不要なクロージャの生成、依存性の結合といった隠れたバグを回避することができます。


理由 単一のコンポーネントを、そのファイルのデフォルトエクスポートにすることができます。これにより、ルータでの遅延読み込みがしやすくなります。

大切なことは、コードをより再利用しやすく、読みやすく、間違いが少ないものにすることです。

次の回避例では、 AppComponentの定義、アプリケーションの起動、Heroモデルオブジェクトの定義、サーバーからのヒーローの呼び出しを、すべて同一ファイルでやっています。 こうはしないでください

app/heroes/hero.component.ts

/* 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;
}

コンポーネントと振舞いのサポートは、それぞれの従属ファイルで再定義した方がよいでしょう。

main.ts
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule }      from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule);
app/app.module.ts
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 { }
app/app/app.component.ts
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 { }
app/heroes/heroes.component.ts
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);
  }
}
app/heroes/shared/hero.service.ts
import { Injectable } from '@angular/core';
import { HEROES } from './mock-heroes';
@Injectable()
export class HeroService {
  getHeroes() {
    return Promise.resolve(HEROES);
  }
}
app/heroes/shared/hero.model.ts
export class Hero {
  id: number;
  name: string;
}
app/heroes/shared/mock-heroes.ts
import { Hero } from './hero.model';
export const HEROES: Hero[] = [
  {id: 1, name: 'Bombasto'},
  {id: 2, name: 'Tornado'},
  {id: 3, name: 'Magneta'},
];

アプリが大きくなるにつれて、このルールはさらに重要になります。

Back to top

スモールファンクション

STYLE 01-02

原則 関数は小さく定義します。


検討 75行以下に制限します。


理由 関数を小さくすると、テストが容易になります(特に1つのアクションで1つの目的を果たすような場合)。


理由 関数を小さくすると、再利用がしやすくなります。


理由 関数を小さくすると、可読性が向上します。


理由 関数を小さくすると、保守性が向上します。


理由 関数を小さくすれば、大きな関数で生じやすい外部スコープとの変数共有や、不要なクロージャの生成、不要な依存性の結合といった隠れたバグを回避できます。

Back to top

命名

命名規則は、保守性と可読性にとって非常に重要です。 このガイドでは、ファイル名とシンボル名による命名規則を推奨しています。

普遍的な命名ガイドライン

STYLE 02-01

原則 すべてのシンボルに一貫した名前を使用します。


原則 シンボルを特徴、タイプの順に記述するパターンに従います。 推奨されるパターンの例は、feature.type.tsです。


理由 命名規則によって一貫性を持つことで、コンテンツを一目で見つけることができるようになります。 プロジェクト内の一貫性は必須で、チーム内での一貫性は重要です。 企業全体で一貫性を保つことができれば、非常に効率的になります。


理由 命名規則を使えば、目的のコードをより速く見つけ、理解しやすくすることができます。


理由 フォルダとファイルの名前は、その意図を明確に伝える必要があります。 例えば app / heroes / hero-list.component.tsであれば、ヒーローのリストを管理するコンポーネントが含まれている可能性があります。


Back to top

ドットとダッシュでファイル名を区切る

STYLE 02-02

原則 説明的な名称を単語で区切るにはダッシュを使用します。


原則 ドットを使用して、説明的な名称とそのタイプを区切ります。.


原則 コンポーネントの機能、タイプの順に記述するパターンに従うならば、すべてのコンポーネントに対して一貫したタイプの名称を使用します。 推奨するパターンの例は feature.type.tsです。


原則 .service.component.pipe.module.directiveを含む従来のタイプ名を使用してください。とはいえ、必要があれば作りすぎに注意しつつ、追加のタイプ名を作成してください。


理由 タイプ名は、ファイルの内容をすばやく識別するできるようになる一貫性を提供してくれます。


理由 エディタやIDEにあるファジー検索の技術を駆使して、特定のファイルタイプを簡単に見つけることができます。


理由 .serviceのような省略されていないタイプ名は説明的であり、はっきりしています。 .srv.svc.servのような略語だと混乱してしまうかもしれません。


理由 自動化したタスクで、パターンマッチングをしてくれるようになります。

Back to top

シンボルとファイル名

STYLE 02-03

原則 その性質にちなんで命名されたすべてのアセットに対して、一貫した名称を使用します。


原則 クラス名にはアッパーキャメルケースを使用してください。 シンボルの名前とファイルの名前を一致させます。


原則 シンボルのタイプ(例: ComponentDirectiveModulePipeService)を通常の接尾辞として付けたシンボル名を追加してください。


原則 ファイルタイプに通常の接尾辞をつけたもの(例:.component.ts.directive.ts.module.ts.pipe.ts.service.ts)をファイル名に加えます。


理由 一貫性によって、アセットを素早く識別し、参照できるようになります。


理由 アッパーキャメルケースは、コンストラクタを使用してインスタンス化できるオブジェクトを識別するために昔から使われているものです。

app.component.ts
@Component({ ... })
export class AppComponent { }
heroes.component.ts
@Component({ ... })
export class HeroesComponent { }
hero-list.component.ts
@Component({ ... })
export class HeroListComponent { }
hero-detail.component.ts
@Component({ ... })
export class HeroDetailComponent { }
validation.directive.ts
@Directive({ ... })
export class ValidationDirective { }
app.module.ts
@NgModule({ ... })
export class AppModule
init-caps.pipe.ts
@Pipe({ name: 'initCaps' })
export class InitCapsPipe implements PipeTransform { }
user-profile.service.ts
@Injectable()
export class UserProfileService { }

Back to top

サービス名

STYLE 02-04

原則 その性質にちなんで命名されたすべてのサービスに対して、一貫した名称を使用します。


原則 サービスにはアッパーキャメルケースを使用します。


原則 その存在が不明瞭なとき(例えば、名詞のとき)、サービスにはServiceを接尾辞として加えます。


理由 一貫性によって、サービスを素早く識別し、参照することができるようになります。


理由 Loggerのような役割が明確なサービス名は、接尾辞を必要としません。


理由 Creditのようなサービス名は名詞であり、接尾辞を必要とします。それがサービスなのか他のものなのか明らかでないときは、接尾辞を付けるべきです。

hero-data.service.ts
@Injectable()
export class HeroDataService { }
credit.service.ts
@Injectable()
export class CreditService { }
logger.service.ts
@Injectable()
export class Logger { }

Back to top

ブートストラップ(起動)

STYLE 02-05

原則 アプリケーションのブートストラップとプラットフォームロジックを main.tsという名前のファイルに入れてください。


原則 ブートストラップのロジックに、エラー処理を含めます。


回避 アプリケーションロジックを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));

Back to top

ディレクティブセレクタ

STYLE 02-06

原則 ディレクティブのセレクタに名前を付けるときは、ローワーキャメルケースを使用してください。


理由 ビューにバインドされたディレクティブで定義され、属性名と一致するプロパティの名前を保持します。


理由 AngularのHTMLパーサーは大文字と小文字を区別し、ローワーキャメルケースを認識します。


Back to top

コンポーネントのカスタム接頭辞

STYLE 02-07

原則 ハイフンで区切られた小文字の要素セレクタ値(例: admin-users)を使用してください。


原則 コンポーネントセレクタのカスタムプレフィックスを使用します。 たとえば、接頭辞 tohはTour of Heroesを表し、接頭辞adminはadmin機能領域を表します。


原則 機能領域またはアプリ自体を識別するプレフィックスを使用します。


理由 他のアプリケーションやネイティブHTML要素のコンポーネントとの、要素名の衝突を防ぎます。


理由 他のアプリにあるコンポーネントを共有したり、助長しやすくします。


理由 コンポーネントはDOMで簡単に識別できます。

app/heroes/hero.component.ts
/* avoid */
// HeroComponent is in the Tour of Heroes feature
@Component({
  selector: 'hero'
})
export class HeroComponent {}
app/users/users.component.ts
/* avoid */
// UsersComponent is in an Admin feature
@Component({
  selector: 'users'
})
export class UsersComponent {}
app/heroes/hero.component.ts
@Component({
  selector: 'toh-hero'
})
export class HeroComponent {}
app/users/users.component.ts
@Component({
  selector: 'admin-users'
})
export class UsersComponent {}

ディレクティブのカスタム接頭辞

STYLE 02-08

原則 ディレクティブのセレクタにカスタムプレフィックス(例:Tour of Heroesの接頭辞 toh)を使用します。


原則 セレクタがネイティブのHTML属性と一致する場合を除いて、ローワーキャメルケースで非要素セレクタを綴ります。


理由 名前の衝突を防ぎます。


理由 ディレクティブが簡単に識別できます。

app/shared/validate.directive.ts
/* avoid */
@Directive({
  selector: '[validate]'
})
export class ValidateDirective {}
app/shared/validate.directive.ts
@Directive({
  selector: '[tohValidate]'
})
export class ValidateDirective {}

Back to top

パイプ名

STYLE 02-09

原則 その性質にちなんで命名されたすべてのパイプに、一貫した名称を使用します。


理由 一貫性によって、パイプを素早く識別し、参照することができるようになります。


ellipsis.pipe.ts
@Pipe({ name: 'ellipsis' })
export class EllipsisPipe implements PipeTransform { }
init-caps.pipe.ts
@Pipe({ name: 'initCaps' })
export class InitCapsPipe implements PipeTransform { }

Back to top

単体テストファイル名

STYLE 02-10

原則 テスト用のファイルは、テストするコンポーネントと同じ名前にします。


原則 テスト用ファイルは、接尾辞.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

Back to top

e2eテストファイル名

STYLE 02-11

原則 テストする機能にちなんでエンドツーエンドのテスト用ファイルを命名するときは、接尾辞.e2e-specをつけます。


理由 一貫性によって、エンドツーエンドのテストが素早く識別できるようになります。


理由 テストランナーとビルド自動化のパターンマッチングを提供します。

シンボル名 ファイル名
End to End Tests app.e2e-spec.ts,
heroes.e2e-spec.ts

Back to top

AngularのNgModule名

STYLE 02-12

原則 シンボル名に接尾辞Moduleを付けます。


原則 ファイル名に.module.ts拡張子を付けます。


原則 モジュールが存在するフォルダーと機能にちなんで、モジュールの名前を付けます。


理由 一貫性によって、モジュールを素早く識別し、参照することができるようになります。


理由 アッパーキャメルケースは、コンストラクタを使用してインスタンス化できるオブジェクトを識別するために昔から使われているものです。


理由 同じ名前の機能をルートとして、モジュールを簡単に識別できます。


原則 RoutingModuleクラス名には、RoutingModuleという接尾辞を付けます。


原則 RoutingModuleのファイル名は、-routing.module.tsで終了します。


理由 RoutingModuleは、Angularルーターの設定専用モジュールです。クラス名とファイル名に共通の命名規則を使うことで、これらのモジュールを簡単に見つけて検証することができます。

app.module.ts
@NgModule({ ... })
export class AppModule { }
heroes.module.ts
@NgModule({ ... })
export class HeroesModule { }
villains.module.ts
@NgModule({ ... })
export class VillainsModule { }
app-routing.module.ts
@NgModule({ ... })
export class AppRoutingModule { }
heroes-routing.module.ts
@NgModule({ ... })
export class HeroesRoutingModule { }

Back to top

コーディング規約

コーディング、命名、および空白の規則には、一貫性をもたせてください。

クラス

STYLE 03-01

原則 クラスに名前を付けるときは、アッパーキャメルケースを使用します。


理由 従来のクラス名の考え方に従います。


理由 クラスをインスタンス化して、インスタンスを構築することができます。 規約により、アッパーキャメルケースはインスタンス化が可能なアセットを示します。

app/shared/exception.service.ts
/* avoid */
export class exceptionService {
  constructor() { }
}
app/shared/exception.service.ts
export class ExceptionService {
  constructor() { }
}

Back to top

定数

STYLE 03-02

原則 アプリケーション動作中に値が変更されるべきでないような場合、変数を constで宣言してください。


理由 値が不変であることを、コードを読む者に伝えます。


理由 TypeScriptでは、即時初期化を要求し、その後の再割り当てを防ぐことで、その意図を強化することができます。


検討 const変数は、ローワーキャメルケースで綴ります。


理由 ローワーキャメルケースの変数名( heroRoutes)は、従来のUPPER_SNAKE_CASE名(HERO_ROUTES)よりも読みやすく理解しやすいです。


理由 UPPER_SNAKE_CASEを使って定数を名づける従来のやり方は、const宣言をすばやく判別することのできる現代IDE以前の時代を反映しています。 TypeScript自体は、偶発的な再割り当てをさせません。


原則 UPPER_SNAKE_CASEで綴られた既存の const変数は許容されます。


理由 UPPER_SNAKE_CASEという古い手法は、特にサードパーティ製のモジュールで広く普及しています。 それらを変更するために努力したり、既存のコードや文書を破壊するリスクをとるほどの価値はほとんどありません。

app/shared/data.service.ts
export const mockHeroes   = ['Sam', 'Jill']; // prefer
export const heroesUrl    = 'api/heroes';    // prefer
export const VILLAINS_URL = 'api/villains';  // tolerate

Back to top

インターフェース

STYLE 03-03

原則 アッパーキャメルケースを使用して、インターフェースに名前を付けます。


検討 Iプリフィックスを付けずにインタフェース名を指定します。


検討 インタフェースの代わりにクラスを使用します。


理由 TypeScriptガイドラインでは、"I" プリフィックスを奨励していません。


理由 クラスだけにすれば、クラスプラスインターフェイスよりもコードが少なくなります。


理由 クラスはインタフェースとしても動作できます(extendsの代わりにimplementsを使用してください)。


理由 interface-classは、AngularDIのプロバイダルックアップトークンにすることができます。


app/shared/hero-collector.service.ts
/* avoid */
import { Injectable } from '@angular/core';
import { IHero } from './hero.model.avoid';
@Injectable()
export class HeroCollectorService {
  hero: IHero;
  constructor() { }
}
app/shared/hero-collector.service.ts
import { Injectable } from '@angular/core';
import { Hero } from './hero.model';
@Injectable()
export class HeroCollectorService {
  hero: Hero;
  constructor() { }
}

Back to top

プロパティとメソッド

STYLE 03-04

原則 ローワーキャメルケースを使用して、プロパティとメソッドの名前を付けます。


回避 プライベートなプロパティとメソッドの接頭辞に、アンダースコアを付ないでください。


理由 プロパティとメソッドの従来の考え方に従います。


理由 JavaScript自体にプライベートなプロパティやメソッドはありません。


理由 TypeScriptツールを使用すると、プライベートとパブリックのプロパティ、メソッドを簡単に識別できます。

app/shared/toast.service.ts
/* 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);
  }
}
app/shared/toast.service.ts
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);
  }
}

Back to top

インポートの行間

STYLE 03-06

検討 サードパーティのインポートとアプリケーションのインポートの間に、空行を1つ残します。


検討 モジュール単位で、アルファベット順にインポート行を並べます。


検討 バラバラにインポートされたアセットをアルファベット順に並べ替えます。


理由 空行は、インポートの読み込みと配置をしやすくします。


理由 アルファベット順にすると、インポートの読み込みと配置がしやすくなります。

app/heroes/shared/hero.service.ts
/* avoid */
import { ExceptionService, SpinnerService, ToastService } from '../../core';
import { Http, Response } from '@angular/http';
import { Injectable } from '@angular/core';
import { Hero } from './hero.model';
app/heroes/shared/hero.service.ts
import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';

import { Hero } from './hero.model';
import { ExceptionService, SpinnerService, ToastService } from '../../core';

Back to top

アプリ構成とAngularモジュール

実装を行うには、短期的なビジョンと長期的なビジョンが必要になります。もちろん最初は小規模から始めるのですが、アプリがどこに向かっているのかを考えながら実装してください。

アプリのコードはすべて、 appという名前のフォルダに入れておきます。 すべての機能領域は、それぞれ独自のAngularモジュールを使用し、独自のフォルダへと格納します。

すべてのコンテンツは、1ファイルにつき1つのアセットとなります。コンポーネント、サービス、およびパイプは、それぞれ1つのファイルに1つずつ入れておきます。サードパーティベンダーのスクリプトは、すべて別のフォルダに保存し、appフォルダには保存しません。自分で書いていないスクリプトで、appフォルダを散らかしたくないですよね。各ファイルには、このガイドで紹介している命名規則を利用してください。

Back to top

LIFT

STYLE 04-01

原則 コードを素早く見つけられること(Locate)、コードを一目で識別できること(Identify)、最もフラットな構造を保つこと(Flattest)、DRYになるよう試みるこ(Try)に配慮して、アプリを構築してください。


原則 これらの4つの基本的なガイドラインに従って構造を定義し、重要度の順に列挙します。


理由 LIFTに従えば、一貫性があってスケールがしやすく、モジュール化された、コードのみつけやすい開発者フレンドリーな構成を作ることができます。独自の構成について自分の直感を確認したいときは、この機能の関連ファイル全てを即時に開き、作業を始められるか?と自問自答してみてください。

Back to top

Locate

STYLE 04-02

原則 直感的で、簡単かつ迅速にコードを見つけられるようにします。


理由 効率的に作業しようとするなら、たとえファイルの名前を知らない(または覚えていない)ようなときでも、すばやくファイルを見つけられるようにしなければいけません。直感的にわかる場所で、かつそれと関連するファイルが近くにあれば、検索にかかる時間が節約されます。わかりやすいフォルダ構成は、あなたとあなたの後にやって来る人々に異世界を提供してくれます。

Back to top

Identify

STYLE 04-03

原則 そのファイルに含まれているものと、その内容がすぐわかるように命名します。


原則 ファイル名で、ファイル内に確実に1つのコンポーネントが含まれているとわかるようにしておきます。


回避 複数のコンポーネント、サービス、またはその混成を含むようなファイルは避けてください。


理由 コードの吟味にかかる時間が短縮され、効率があがります。 長いファイル名は、短くて不明瞭な省略名よりはるかに優れています。

小規模で親和性の高い機能が多く、複数のファイルにまたがるよりも1つのファイルにまとめた方が発見も理解も進むような場合、one-thing-per-file の法則から外れた方がよいこともあります。こういった抜道には注意してください。

Back to top

Flat

STYLE 04-04

原則 できるだけフラットなフォルダ構成を保ってください。


検討 フォルダに7つ以上のファイルがあるのであれば、サブフォルダを作成します。


検討 生成された.jsファイルや.js.mapファイルのような無関係なファイルは隠すようにIDEを設定します。


理由 7階層もあるフォルダの中からからファイルを検索したいとは誰も思いません。フラットな構造であれば、簡単にスキャンできます。

一方心理学では、面白そうなものが10以上並んでいると悩み始めるとされています。なので1つのフォルダに10個以上のファイルがある場合は、サブフォルダを作成する必要があります。

あなたが快適だと思う階層になることを心掛けて決定してください。新しいフォルダを作成するに足る明白な価値が生じるまでは、フラットな構造を使用しましょう。

Back to top

T-DRY (DRYになるよう試みる)

STYLE 04-05

原則 DRYにしてください。 (同じものを繰り返さないでください。)


回避 可読性を犠牲にしてまで、DRYにする必要はありません。


理由 DRYは重要ですが、LIFTの他の要素を犠牲にするほどではありません。 それが、T-DRYと呼ばれる由縁です。 たとえば、コンポーネントが明示的にビューであるため、コンポーネントの名前を hero-view.component.htmlとするのは冗長です。 しかし、わかりづらいものがある、または慣例から外れているような場合は、それを書き出すようにしてください。

Back to top

全体的な構成のガイドライン

STYLE 04-06

原則 小規模から始めたとしても、アプリがどこに向かっているのかを考えながら実装してください。


原則 短期的なビジョンと長期的なビジョンを持って実装してください。


原則 すべてのアプリケーションのコードを 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つのファイルが追加されますが、フォルダの入れ子構造も縮小されます。どちらを選んでも、一貫性は担保されます。

Back to top

機能別のフォルダ構成

STYLE 04-07

原則 フォルダには、その機能領域を表現した名前をつけて作成してください。


理由 構成がフラットで、かつ反復も冗長性もない命名がされていれば、開発者はコードを見つけ、そのファイルの機能がどんなものであるかを一目で判断できるようになります。


理由 LIFTガイドラインすべてに適用されます。


理由 コンテンツを整理し、LIFTのガイドラインに準拠しておくことで、アプリが乱雑になるのを防ぎます。


理由 ファイルがたくさんある場合(10以上)、フォルダ構成に一貫性があれば目的のファイルを見つけやすくなりますが、フラットな構成では見つけるのが難しくなります。


原則 機能領域ごとにAngularモジュールを作成します。


理由 Angularモジュールは、ルーティング可能な機能の遅延読み込みをしやすくします。


理由 Angularモジュールは、機能の分離、テスト、再利用をしやすくします。

このフォルダとファイルの構成は、この例を参照してください。

Back to top

Appルートモジュール

STYLE 04-08

原則 アプリのルートフォルダにAngularモジュールを作成します(例:/appの中)。


理由 すべてのアプリには、少なくとも1つルートAngularモジュールが必要です。


検討 ルートモジュールは app.module.tsという名前にします。


理由 ルートモジュールの特定と識別が容易になります。

app/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 {}

Back to top

機能モジュール

STYLE 04-09

原則 アプリにあるすべての異なる機能(例:Heroes機能)に対してAngularモジュールを作成します。


原則 機能モジュールを、機能領域と同じ名前のフォルダに配置します(例:app/heroes)。


原則 機能領域とフォルダの名前を反映して、機能モジュールファイルの名前を付けます(例:app/heroes/heroes.module.ts)。


原則 機能領域、フォルダ、ファイルの名前を反映して、機能モジュールのシンボルに名前を付けます(例:app/heroes/heroes.module.tsであれば、HeroesModule)。


理由 機能モジュールは、他のモジュールに対して実装を公開、非公開にすることができます。


理由 機能モジュールは、機能領域を構成する関連コンポーネントのセットを識別します。


理由 機能モジュールは、一括、遅延のどちらでも簡単にルーティングできます。


理由 機能モジュールは、特定の機能と他のアプリケーション機能との明確な境界を定義します。


理由 機能モジュールは、開発チーム間の責任を明確にしやすくします。


理由 機能モジュールは、テスト用の分離をしやすくします。

Back to top

共有機能モジュール

STYLE 04-10

原則 sharedフォルダにSharedModuleという名前の機能モジュールを作成します(例:app/shared/shared.module.tsであればSharedModule)。


原則 SharedModule内の他の機能モジュールによってアプリケーション全体で使用される共通コンポーネント、ディレクティブおよびパイプを配置します。これらのアセットでは、シングルトンではない新しいインスタンスを共有することが期待されます。


原則 SharedModuleでアセットに必要なすべてのモジュールをインポートします(例:CommonModuleFormsModule)。


理由 SharedModuleには、他の共通モジュールにある機能が必要なコンポーネント、ディレクティブ、パイプが含まれています(例:CommonModulengFor)。


原則 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
└...
app/shared/shared.module.ts
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 { }
app/shared/init-caps.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'initCaps' })
export class InitCapsPipe implements PipeTransform {
  transform = (value: string) => value;
}
app/shared/filter-text/filter-text.component.ts
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);
  }
}
app/shared/filter-text/filter-text.service.ts
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;
  }
}
app/heroes/heroes.component.ts
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;
  }
}
app/heroes/heroes.component.html
<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>

Back to top

コア機能モジュール

STYLE 04-11

原則 単一利用のクラスを集めて、CoreModuleの中にその見せたくない詳細部分を隠します。 簡略化されたルートの AppModuleは、アプリケーション全体のオーケストレーターとしての能力でCoreModuleをインポートします。


原則 コアフォルダにCoreModuleという名前の機能モジュールを作成します(例:app/core/core.module.tsであれば、CoreModule


原則 インスタンスがアプリケーション全体で共有されるシングルトンサービスを、Core Moduleに配置します(例:ExceptionServiceLoggerService)。


原則 CoreModuleのアセットで要求されるモジュールをすべてインポートします(例:CommonModuleFormsModule)。


理由 CoreModuleは、1つ以上のシングルトンサービスを提供します。 Angularはプロバイダにアプリのルートインジェクタを登録し、一括、もしくは遅延して読み込まれたコンポーネントが各サービスのシングルトンインスタンスを利用できるようにします。


理由 CoreModuleはシングルトンサービスを含みます。 遅延読み込みされたモジュールがこれらをインポートすると、意図されたアプリケーション全体のシングルトンではなく、新しいインスタンスが取得されます。


原則 CoreModuleに、アプリケーション全体で1回しか使用しないコンポーネントを集めます。 アプリケーションを起動するときに一度(AppModuleの中で)それをインポートして、他のどこからもインポートしないでください(例:NavComponentSpinnerComponent)。


理由 現実世界のアプリケーションでは、AppComponentテンプレートにのみ現れる複数の使い捨てコンポーネント(例えばスピナー、メッセージトースト、モーダルダイアログ)を持つことができます。 それらは他の場所ではインポートされないので、その意味では共有されるというわけではありませんが、大きくて面倒なものをルートフォルダに残したままにはしておけません。


回避 CoreModuleAppModule以外のところからインポートしないでください。


理由 遅延読み込みでCoreModuleを直接インポートした機能モジュールは、サービス自身のコピーを作成し、望ましくない結果をもたらす可能性があります。


理由 一括読み込みをされた時点で、機能モジュールはAppModuleのインジェクタ、つまりCoreModuleのサービスにアクセスしています。


原則 AppModuleCoreModuleをインポートし、他の機能モジュールからも利用できるように、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
└...
app/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';
import { CoreModule }    from './core/core.module';
@NgModule({
  imports: [
    BrowserModule,
    CoreModule,
  ],
  declarations: [
    AppComponent,
    HeroesComponent
  ],
  exports: [ AppComponent ],
  entryComponents: [ AppComponent ]
})
export class AppModule {}
app/core/core.module.ts
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 { }
app/core/logger.service.ts
import { Injectable } from '@angular/core';
@Injectable()
export class LoggerService {
  log(msg: string) {
    console.log(msg);
  }
  error(msg: string) {
    console.error(msg);
  }
}
app/core/nav/nav.component.ts
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() { }
}
app/core/nav/nav.component.html
<header>
  <div>
    <h4>Tour of Heroes</h4>
  </div>
  <nav>
    <ul>
      <li *ngFor="let item of menuItems">
        {{item}}
      </li>
    </ul>
  </nav>
  <br/>
</header>
app/core/spinner/spinner.component.ts
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();
  }
}
app/core/spinner/spinner.component.html
<div class="spinner" [class.spinner-hidden]="!visible"> </div>
app/core/spinner/spinner.service.ts
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はアプリ全体をオーケストレーションするという、メインのタスクに集中させておきます。

Back to top

コアモジュールの再インポートを防ぐ

STYLE 04-12

CoreModuleをインポートするのは、ルートにあるAppModuleだけにしておくべきです。

原則 ガードロジックを追加することでCoreModuleの再インポートを防ぎ、高速化させます。


理由 CoreModuleの再インポートを防ぎます。


理由 シングルトンを意図したアセットが、複数のインスタンスをもってしまうことを防ぎます。

app/core/module-import-guard.ts
export function throwIfAlreadyLoaded(parentModule: any, moduleName: string) {
  if (parentModule) {
    throw new Error(`${moduleName} has already been loaded. Import Core modules in the AppModule only.`);
  }
}
app/core/core.module.ts
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');
  }
}

Back to top

遅延読み込みをしたフォルダ

STYLE 04-13

個別のアプリケーション機能またはワークフローが、アプリケーションの起動時ではなく、遅延読み込みまたはオンデマンド読み込みされることがあります。

原則 遅延読み込みされる機能は、遅延読み込みフォルダに入れてください。遅延読み込みフォルダには、ルーティングコンポーネントとその子コンポーネント、および関連するアセットとモジュールなどが典型として含まれます。


理由 このフォルダを使用すると、機能の内容を簡単に識別して切り分けることができます。

Back to top

遅延読み込みフォルダを直接インポートしない

STYLE 04-14

回避 兄弟フォルダや親フォルダ内にあるモジュールが、遅延読み込み機能でモジュールを直接インポートしないようにしてください。

理由 モジュールを直接インポートして使用すると、オンデマンド読み込みを意図しているような場合であっても、すぐに読み込みが開始されます。

Back to top

コンポーネント

コンポーネントセレクタの命名

STYLE 05-02

原則 コンポーネントのセレクタ要素を命名するときは、ダッシュケースまたはケバブケースを使用します。


理由 要素名をカスタム要素の仕様と一致させておきます。

app/heroes/shared/hero-button/hero-button.component.ts
/* avoid */
@Component({
  selector: 'tohHeroButton',
  templateUrl: 'hero-button.component.html'
})
export class HeroButtonComponent {}
app/heroes/shared/hero-button/hero-button.component.ts
@Component({
  selector: 'toh-hero-button',
  templateUrl: 'hero-button.component.html'
})
export class HeroButtonComponent {}
app/app.component.html
<toh-hero-button></toh-hero-button>

Back to top

要素としてのコンポーネント

STYLE 05-03

原則 セレクタを介してコンポーネントを要素として定義します。


理由 コンポーネントには、HTMLとオプションのAngularテンプレートシンタックスを含むテンプレートがあります。 それらはページ上のコンテンツ配置と非常に関連性が高く、要素を使って並べる方がより親和性が高くなります。


理由 コンポーネントは、ページ上のビジュアル要素を表現します。 HTML要素タグとしてセレクタ定義するのは、ネイティブのHTML要素やWebComponentsと同じです。


理由 テンプレートのhtmlを見れば、シンボルがコンポーネントなのかディレクティブなのかを簡単に認識できます。

app/heroes/hero-button/hero-button.component.ts
/* avoid */
@Component({
  selector: '[tohHeroButton]',
  templateUrl: 'hero-button.component.html'
})
export class HeroButtonComponent {}
app/app.component.html
<!-- avoid -->
<div tohHeroButton></div>
app/heroes/shared/hero-button/hero-button.component.ts
@Component({
  selector: 'toh-hero-button',
  templateUrl: 'hero-button.component.html'
})
export class HeroButtonComponent {}
app/app.component.html
<toh-hero-button></toh-hero-button>

Back to top

自分のファイルにテンプレートとスタイルを抽出する

STYLE 05-04

原則 3行以上になった場合、テンプレートとスタイルを別々のファイルに抽出します。


原則 テンプレートファイルは[component-name].component.htmlという風に名前を付けます。ここでの[component-name]はコンポーネント名です。


原則 スタイルファイルは[component-name].component.cssという風に名前を付けます。ここでの[component-name]はコンポーネント名です。


理由 (.jsや.ts)コードファイル内にあるインラインのテンプレートだと、シンタックスヒントがサポートされていないエディタがあります。


理由 コンポーネントファイルのロジックは、テンプレートやスタイルがインラインで混ざっていない方が読みやすいです。

app/heroes/heroes.component.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() {}
}
app/heroes/heroes.component.ts
@Component({
  selector: 'toh-heroes',
  templateUrl: 'heroes.component.html',
  styleUrls:  ['heroes.component.css']
})
export class HeroesComponent implements OnInit {
  heroes: Hero[];
  selectedHero: Hero;
  ngOnInit() { }
}
app/heroes/heroes.component.html
<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>
app/heroes/heroes.component.css
.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;
}

Back to top

InputとOutputのプロパティをインラインで書く

STYLE 05-12

原則 @Directive@Componentデコレータのinputsoutputsプロパティの代わりに@Input@Outputを使います。


原則 @Input()または@Output()を、対象となるプロパティと同じ行に置きます。


理由 クラス内のどのプロパティがinputsまたはoutputsであるかを識別することで、より簡単で読みやすいものになります。


理由 @Inputまたは@Outputに関連付けられたプロパティやイベントの名前を変更する必要があっても、1か所の変更で済みます。


理由 ディレクティブに付随するメタデータの宣言は、より短く、より読みやすくなっています。


理由 デコレータを同じ行に配置すると、コードが短くなり、プロパティをinputまたはoutputとして簡単に識別できます。

app/heroes/shared/hero-button/hero-button.component.ts
/* avoid */
@Component({
  selector: 'toh-hero-button',
  template: `<button></button>`,
  inputs: [
    'label'
  ],
  outputs: [
    'change'
  ]
})
export class HeroButtonComponent {
  change = new EventEmitter<any>();
  label: string;
}
app/heroes/shared/hero-button/hero-button.component.ts
@Component({
  selector: 'toh-hero-button',
  template: `<button>{{label}}</button>`
})
export class HeroButtonComponent {
  @Output() change = new EventEmitter<any>();
  @Input() label: string;
}

Back to top

InputsとOutputsのリネームを避ける

STYLE 05-13

回避 できるだけinputsとoutputsのリネームはしないでください。


理由 指定されたディレクティブのoutputまたはinputプロパティが指定されたものの、パブリックAPIとは異なる名前でエクスポートされると、混乱することがあります。

app/heroes/shared/hero-button/hero-button.component.ts
/* avoid */
@Component({
  selector: 'toh-hero-button',
  template: `<button>{{label}}</button>`
})
export class HeroButtonComponent {
  @Output('changeEvent') change = new EventEmitter<any>();
  @Input('labelAttribute') label: string;
}
app/app.component.html
<!-- avoid -->
<toh-hero-button labelAttribute="OK" (changeEvent)="doSomething()">
</toh-hero-button>
app/heroes/shared/hero-button/hero-button.component.ts
@Component({
  selector: 'toh-hero-button',
  template: `<button>{{label}}</button>`
})
export class HeroButtonComponent {
  @Output() change = new EventEmitter<any>();
  @Input() label: string;
app/app.component.html
<toh-hero-button label="OK" (change)="doSomething()">
</toh-hero-button>

Back to top

メンバの順序

STYLE 05-14

原則 プロパティを上部に配置し、その下にメソッドを配置します。


原則 publicのメンバの後に、privateのメンバをアルファベット順に配置します。


理由 メンバを一貫性のある順序で並べることで、可読性が向上し、コンポーネントのどのメンバがどの目的に役立つかを即座に判断しやすくなります。

app/shared/toast/toast.component.ts
/* 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);
  }
}
app/shared/toast/toast.component.ts
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);
  }
}

Back to top

サービスにロジックを入れる

STYLE 05-15

原則 コンポーネント内のロジックをビューに必要なロジックのみに制限します。 他のすべてのロジックはサービスに委任されるべきです。


原則 再利用可能なロジックはサービスに移行し、コンポーネントは簡潔に保ちつつ、意図した目的に集中させます。


理由 ロジックをサービス内に配置し関数を介して公開すれば、複数のコンポーネント間で再利用することができます。


理由 サービス内にロジックがあれば、ユニットテストで簡単に分離でき、コンポーネント内でロジックを呼び出せば、簡単にモックとして利用できます。
理由 依存関係を削除し、コンポーネントから実装の細部が見えないようにします。


理由 コンポーネントは、小さく整理された、その役割に集中したものにしておきます。

app/heroes/hero-list/hero-list.component.ts
/* 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
  }
}
app/heroes/hero-list/hero-list.component.ts
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();
  }
}

Back to top

Outputプロパティに接頭辞を付けない

STYLE 05-16

原則 イベントにはon接頭辞をつけずに命名してください。


原則 イベントハンドラメソッドは、接頭辞 onの後にイベント名が続くように命名してください。


理由 この命名規則は、ボタンクリックなどのビルトインイベントと同様です。


理由 Angularは代替構文on-*を許容します。イベント自体に接頭辞onがある場合、これはon-onEventバインディング方式になります。

app/heroes/hero.component.ts
/* avoid */
@Component({
  selector: 'toh-hero',
  template: `...`
})
export class HeroComponent {
  @Output() onSavedTheDay = new EventEmitter<boolean>();
}
app/app.component.html
<!-- avoid -->
<toh-hero (onSavedTheDay)="onSavedTheDay($event)"></toh-hero>
app/heroes/hero.component.ts
export class HeroComponent {
  @Output() savedTheDay = new EventEmitter<boolean>();
}
app/app.component.html
<toh-hero (savedTheDay)="onSavedTheDay($event)"></toh-hero>

Back to top

コンポーネントクラスにプレゼンテーションロジックを配置する

STYLE 05-17

原則 プレゼンテーションロジックはコンポーネントクラスに入れ、テンプレートには入れない。


理由 ロジックは2つの場所に分散するのではなく、1つの場所(コンポーネントクラス)に集約されるようにします。


理由 テンプレートの代わりに、コンポーネントクラスにあるプレゼンテーションロジックを維持することで、テスト容易性、保守性、および再利用性が向上します。


app/heroes/hero-list/hero-list.component.ts
/* 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;
}
app/heroes/hero-list/hero-list.component.ts
@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;
  }
}

Back to top

ディレクティブ

ディレクティブを使用して既存要素を強化する

STYLE 06-01

原則 テンプレートのないプレゼンテーションロジックを使用する場合は、属性ディレクティブを使用します。


理由 属性ディレクティブには、関連するテンプレートがありません。


理由 要素には複数の属性ディレクティブが適用されている場合があります。

app/shared/highlight.directive.ts
@Directive({
  selector: '[tohHighlight]'
})
export class HighlightDirective {
  @HostListener('mouseover') onMouseEnter() {
    // do highlight work
  }
}
app/app.component.html
<div tohHighlight>Bombasta</div>

Back to top

HostListenerおよびHostBindingクラスのデコレータを使用

STYLE 06-03

検討 @HostListener@HostBinding@Directive@Componentデコレータのhostプロパティよりも優先させます。


原則 一貫性を持って選択してください。


理由 @HostBindingに関連するプロパティ、または@HostListenerに関連するメソッドは、ディレクティブクラス内にある単一の場所からのみ変更できるようになります。hostメタデータのプロパティを使用する場合は、コントローラ内のプロパティ宣言と、そのディレクティブに関連するメタデータの両方を変更する必要があります。

app/shared/validator.directive.ts
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インポートを必要としません。

app/shared/validator2.directive.ts
import { Directive } from '@angular/core';
@Directive({
  selector: '[tohValidator2]',
  host: {
    'attr.role': 'button',
    '(mouseenter)': 'onMouseEnter()'
  }
})
export class Validator2Directive {
  role = 'button';
  onMouseEnter() {
    // do work
  }
}

Back to top

サービス

サービスはインジェクタ内にあるシングルトン

STYLE 07-01

原則 同じインジェクタ内にあるシングルトンとしてサービスを使用してください。サービスはデータと機能の共有を行う場合に使用します。


理由 サービスは、機能領域やアプリ全体でのメソッドの共有に最適です。


理由 サービスは、状態を持ったメモリ内にあるデータの共有に最適です。

app/heroes/shared/hero.service.ts
export class HeroService {
  constructor(private http: Http) { }
  getHeroes() {
    return this.http.get('api/heroes')
      .map((response: Response) => <Hero[]>response.json().data);
  }
}

Back to top

単一責任

STYLE 07-02

原則 コンテキストを元にカプセル化するという単一責任の原則に従って、サービスを作成します。


原則 サービスがその単体の目的を超え始めたとき、新しいサービスを作ります。


理由 サービスに複数の責任があると、テストするのが難しくなります。


理由 サービスに複数の責任があると、そのサービスをインジェクトするすべてのコンポーネント、またはサービスが、そのすべての重圧を受けてしまいます。

Back to top

サービスの提供

STYLE 07-03

原則 共有されている中で、最上位にあるコンポーネントのAngularインジェクタにサービスを提供します。


理由 Angularインジェクタは階層構造になっています。


理由 最上位にあるコンポーネントにサービスを提供する場合、そのインスタンスは共有され、その最上位コンポーネントのすべての子コンポーネントで利用できます。


理由 この手法はサービスがメソッドや状態を共有しているときに最適です。


理由 2つの異なるコンポーネントが1つのサービスの異なるインスタンスをそれぞれ必要とするような場合、この手法は理想的ではありません。このシナリオでは、新規および別個のインスタンスを必要とするコンポーネントレベルでサービスを提供する方がよいでしょう。

app/app.component.ts
import { Component } from '@angular/core';
import { HeroService } from './heroes';
@Component({
  selector: 'toh-app',
  template: `
      <toh-heroes></toh-heroes>
    `,
  providers: [HeroService]
})
export class AppComponent {}
app/heroes/hero-list/hero-list.component.ts
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);
  }
}

Back to top

@Injectable()クラスデコレータを使う

STYLE 07-04

原則 サービスの依存関係の型をトークンとして使用する場合は、@Inject parameter decoratorの代わりに@Injectableクラスのデコレータを使用してください。


理由 Angular DIのメカニズムは、サービスのコンストラクタで宣言された型に基づいて、サービスのすべての依存関係を解決します。


理由 サービスが型トークンに関連付けられた依存関係のみを受け入れる場合、@Injectable()シンタックスは、個々のコンストラクタパラメータで @Inject()を使用するよりもはるかに冗長になります。

app/heroes/shared/hero-arena.service.ts
/* avoid */
export class HeroArena {
  constructor(
      @Inject(HeroService) private heroService: HeroService,
      @Inject(Http) private http: Http) {}
}
app/heroes/shared/hero-arena.service.ts
@Injectable()
export class HeroArena {
  constructor(
    private heroService: HeroService,
    private http: Http) {}
}

Back to top

データサービス

個別のデータ呼び出し

STYLE 08-01

原則 データ操作、およびサービス間とのデータのやり取りを行うロジックをリファクタリングします。


原則 データサービスがXHRコール、ローカルストレージ、メモリ内スタッシュ、その他のデータ操作の役割を担います。


理由 コンポーネントの役割は、ビュー情報の表示と収集です。 どのようにしてデータを取得するのかを気にする必要はなく、誰かがデータを要求していることがわかってさえいればいいのです。データサービスを分離することで、データサービスにアクセス方法のロジックが移動し、コンポーネントがよりシンプルになってビューに集中できるようになります。


理由 これによって、データサービスを使用するコンポーネントをテストするときに、データ呼び出し(モックまたは実データ)のテストが容易になります。


理由 データサービスの実装には、データリポジトリを扱うための非常に特殊なコードが含まれている場合があります。 これには、ヘッダー、データとの対話方法、またはHttpなどの他のサービスが含まれます。 ロジックをデータサービスに分離することは、このロジックを単一の場所にカプセル化して、外部のコンシューマ(おそらくコンポーネント)から実装を隠し、実装を簡単に変更できるようにします。

Back to top

ライフサイクルフック

ライフサイクルフックを使用し、Angularで公開されている重要なイベントを利用します。

Back to top

Implement Lifecycle Hooks Interfaces

STYLE 09-01

原則 ライフサイクルフックインタフェースを実装します。


理由 強力に型付けされたメソッドシグネチャです。コンパイラとエディタは、スペルミスを喚起してくれます。

app/heroes/shared/hero-button/hero-button.component.ts
/* avoid */
@Component({
  selector: 'toh-hero-button',
  template: `<button>OK<button>`
})
export class HeroButtonComponent {
  onInit() { // misspelled
    console.log('The component is initialized');
  }
}
app/heroes/shared/hero-button/hero-button.component.ts
@Component({
  selector: 'toh-hero-button',
  template: `<button>OK</button>`
})
export class HeroButtonComponent implements OnInit {
  ngOnInit() {
    console.log('The component is initialized');
  }
}

Back to top

付録

Angularの便利なツールとヒント

Back to top

Codelyzer

STYLE A-01

原則 このガイドに従って、codelyzerを使用してください。


検討 必要に応じてコードライザーのルールを調整してください。

Back to top

ファイルテンプレートとスニペット

STYLE A-02

原則 一貫性のあるスタイル、パターンに準拠しやすくするため、ファイルのテンプレートやスニペットを使用ください。 WEB開発用のエディタやIDEであれば、テンプレートやスニペットがあります。

検討 ここまで紹介してきたスタイルとガイドラインに準拠しているVisual Studio Codeにあるスニペットを使用してください。

Use Extension

検討 ここまで紹介してきたスタイルとガイドラインに準拠しているAtomにあるスニペットを使用してください。


検討 ここまで紹介してきたスタイルとガイドラインに準拠しているSublime Textにあるスニペットを使用してください。


検討 ここまで紹介してきたスタイルとガイドラインに準拠しているVimにあるスニペットを使用してください。

Back to top

15
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
14