AnularJSのチュートリアルをやってみた
Angularのチュートリアルを通して使い方を覚えたいと思います。
今回学習用で作成したプロジェクトは以下からとってこれるようにしています。
GitHub - teruuuuuu/angular_handson
- 開発環境構築
- 開発
- 触ってみた感想
開発環境構築
angular-cliを使えるようにする
npm install -g @angular/cli
2.プロジェクトを作ってみる
ng new my-app
3.アプリを起動する
cd my-app
ng serve –open
“ng serve"で開発用のサーバを起動しファイルに対して変更があればすぐに修正を反映する動きをします。–openのオプションをつけることでコマンド実行時にブラウザを自動で開きます。
Atomで開発
Atomで開発する場合はプラグインを探して入れておけば良さそうです。 atom-typescriptをとりあえず入れておいた
開発
angular-cliでのアプリ開発について
.angular-cli.json
開発環境の設定は.angular-cli.jsonで行なっており、最初に読み込みhtmlやスクリプト、ルートディレクトリやビルド時の出力先もここで設定している。アプリ全体でスタイルやスクリプトを適用したいという場合はここのscriptとstylesや、デフォルトのstyles.cssを修正する。アプリのルートディレクトリはsrcに設定してある。
{ "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "project": { "name": "my-app" }, "apps": [ { "root": "src", "outDir": "dist", "assets": [ "assets", "favicon.ico" ], "index": "index.html", "main": "main.ts", "polyfills": "polyfills.ts", "test": "test.ts", "tsconfig": "tsconfig.app.json", "testTsconfig": "tsconfig.spec.json", "prefix": "app", "styles": [ "styles.css" ], "scripts": [], "environmentSource": "environments/environment.ts", "environments": { "dev": "environments/environment.ts", "prod": "environments/environment.prod.ts" } } ], "e2e": { "protractor": { "config": "./protractor.conf.js" } }, "lint": [ { "project": "src/tsconfig.app.json" }, { "project": "src/tsconfig.spec.json" }, { "project": "e2e/tsconfig.e2e.json" } ], "test": { "karma": { "config": "./karma.conf.js" } }, "defaults": { "styleExt": "css", "component": {} } }
index.html
angular-cliで作成したプロジェクトのindex.htmlは以下のようになっている。app-rootという独自のタグを読み込んでいます。コンポーネントが初期化されるまではタグの中身の"Loading…“が表示されます。
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>AngularHandson</title> <base href="/"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/x-icon" href="favicon.ico"> </head> <body> <app-root>Loading...</app-root> </body> </html>
main.ts
main.tsではアプリに必要なモジュールを読み込んでいます。環境変数などはJsonを読み込ませるかプロパティに直接定義するなどしてenvironmentで読み込めるようにしておくと良さそうです。リリース時はenableProdModeが有効になるようですが、angularの開発モードが無効になるなどの違いがあるようです。
import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './main.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } platformBrowserDynamic().bootstrapModule(AppModule);
main.module.ts
main.tsで読み込んでいるモジュール。NgModuleでページの単位となるコンポーネント一覧を読み込んでいるのが確認できる。bootstrapはエントリポイントを指定していて、providersにはデータ共有に使うサービスとかを指定して、importsでは外部のモジュールを読み込んだりしていて、declarationsではディレクティブとパイプを読み込ませるらしい。ディレクティブとは先ほどのapp-rootなど独自に定義したタグのことを言っており、パイプについてはangularのhtml内で条件指定でフィルターかけるのに使う。BrowserModuleはブラウザの情報を取ってくるので必要になるっぽい。
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
index.component.ts
Componentアノテーションのselectorではディレクティブと使用するタグ名、templeteUrlでテンプレートhtml、styleUrlsでcssを指定してからclassないでスクリプトを記述している。titleはクラス変数でapp.component.htmlの表示で使用している。TypeScriptのクラス変数ではconstやletを付与しようとしたらコンパイラに怒られたので普遍にする場合はstatic readonlyとかつけたら良さそうと思ったけどそうしたらbindできなくなる動きをしていた。
import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './index.component.html', styleUrls: ['./index.component.css'] }) export class AppComponent {}
index.component.html
index.component.tsのhtml表示用のテンプレートは最初は以下のようになっています。
<!--The whole content below can be removed with the new code.--> <div style="text-align:center"> <h1> </h1> </div>
htmlとコンポーネントで2wayバインディングさせてみる
まずmain.module.tsで2wayバインディングに必要となるFormModuleを読み込ませます main.module.ts
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; //テンプレートでバインディングしたり、validationするのに必要 import { AppComponent } from './index.component'; @NgModule({ imports: [ BrowserModule, FormsModule ], declarations: [ AppComponent ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
それからindex.component.tsとindex.component.htmlでコンポーネント内のクラス変数をbindさせてみます。 index.component.ts
import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './index.component.html', styleUrls: ['./index.component.css'] }) export class AppComponent { textInput = "input test"; }
index.component.html
<!--The whole content below can be removed with the new code.--> <div style="text-align:center"> <h1> <input [(ngModel)]="textInput"><br /> {{ textInput }} </h1> </div>
これで実際に動かしてみると画面にコンポーネントのクラス変数が表示され、またinputタグへ入力を行いbindしている変数を変更すると値自体が変更され表示にも反映されるのが確認できます。
ルータとサービスを使ってみる
angularのルータとサービスを利用してみたいと思います。angularのルータにより表示するコンポーネントを切り替えたりURLパラメータを受け取った処理ができるようになります。サービスはAPIを投げてデータを取得したりとか、コンポーネント間で共有するデータとかデータに関する操作とかを記述します。 まずルータを足してみます。 app/router/app.router.ts
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { HeroDetailComponent } from 'app/component/heroDetail/hero.detail.component'; // コンポーネントとURLを関連づける const routes: Routes = [ { path: '', redirectTo: '/detail/1', pathMatch: 'full' }, // URLパラメータをコンポーネントに渡すようにしている { path: 'detail/:id', component: HeroDetailComponent }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
app/service/hero.service.ts
import { Injectable } from '@angular/core'; import { Hero } from 'app/model/Hero'; import { HEROES } from 'app/mock/heros.mock'; @Injectable() export class HeroService { getHeroes(): Promise<Hero[]> { console.log("hero service getHeros"); console.info(HEROES); // 2wayバインドによりmockオブジェクト自体が変更されていることが確認できる return Promise.resolve(HEROES); } getHeroesSlowly(): Promise<Hero[]> { return new Promise(resolve => { // Simulate server latency with 2 second delay setTimeout(() => resolve(this.getHeroes()), 2000); }); } // id指定でデータ取得 getHeroById(id: number): Promise<Hero> { return this.getHeroes() .then(heroes => heroes.find(hero => hero.id === id)); } }
サービスはインポートしたHEROESオブジェクトをPromiseにより非同期でレスポンスとして返す処理を行います。今回使用しているモック用のオブジェクトは以下のようになっています。 app/mock/heros.mock.ts
import { Hero } from 'app/model/Hero'; export const HEROES: Hero[] = [ { id: 1, name: 'Mr. Nick' }, { id: 11, name: 'Mr. Nice' }, { id: 12, name: 'Narco' }, { id: 13, name: 'Bombasto' }, { id: 14, name: 'Celeritas' }, { id: 15, name: 'Magneta' }, { id: 16, name: 'RubberMan' }, { id: 17, name: 'Dynama' }, { id: 18, name: 'Dr IQ' }, { id: 19, name: 'Magma' }, { id: 20, name: 'Tornado' } ];
それと、Heroの型は以下のようになっています。 app/model/Hero.ts
export class Hero { id: number; name: string; }
次にルータとサービスをモジュールとして使えるようにコンポーネントに読み込ませます。 main.module.ts
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; //テンプレートでバインディングしたり、validationするのに必要 import { AppComponent } from 'index.component'; import { HeroDetailComponent } from 'app/component/heroDetail/hero.detail.component'; import { HeroService } from 'app/service/hero.service'; import { AppRoutingModule } from 'app/router/app.router'; @NgModule({ imports: [ AppRoutingModule, // 注意 ルータはdeclationではなくimportsにたす BrowserModule, FormsModule ], declarations: [ AppComponent, HeroDetailComponent ], providers: [ HeroService // サービスはprovidersに追加する ], bootstrap: [AppComponent] }) export class AppModule { }
次にルータとサービスで使用するHeroDetailComponentを追加してみたいと思います。
app/component/heroDetail/hero.detail.component.ts
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Params } from '@angular/router'; import { Hero } from 'app/model/Hero'; import { HeroService } from 'app/service/hero.service'; @Component({ selector: 'hero-detail', //ディレクティブのタグ名 templateUrl: './hero.detail.component.html' //htmlテンプレートの読み込み }) export class HeroDetailComponent implements OnInit { // テンプレートhtmlにbindして使用するクラス変数 title = 'HeroDetail'; hero: Hero = new Hero(); // コンポーネントを使用する側で用途を決めれるようにする isSearchMode: Boolean = true; constructor( private heroService: HeroService, // urlパラメータを取得するのに必要 private route: ActivatedRoute) { } ngOnInit(): void { if(this.isSearchMode){ // ルータからパラメータ取得 this.route.params.forEach((params: Params) => { console.log("hero detail component ngOnInit"); console.info(params); if (params['id'] !== undefined) { const id = +params['id']; this.heroService.getHeroById(id) .then(hero => this.hero = hero); } }); } } }
app/component/heroDetail/hero.detail.component.html
<!-- heroが見つかった時のみこの部分を表示する--> <div *ngIf="hero"> <h1>{{title}}</h1> <h2>{{hero.name}} details!</h2> <div><label>id: </label>{{hero.id}}</div> <div> <label>name: </label> <!-- 注意 ngMoelを使う場合はNgModuleでFormsModuleをインポートしないといけない--> <input [(ngModel)]="hero.name" placeholder="name"> </div> </div> <!-- heroが見つからなかった場合の処理 --> <div *ngIf="!hero"> hero not found. </div>
app/component/heroDetail/hero.detail.component.css
h1 { color: #369; font-family: Arial, Helvetica, sans-serif; font-size: 250%; }
それから、ルータのディレクティブをhtmlテンプレートに追加して使用できるようにします。 index.component.html
<!--The whole content below can be removed with the new code.--> <div style="text-align:center"> <h1> <input [(ngModel)]="textInput"><br /> {{ textInput }}<br /> </div> <!-- ルータ配置用のテンプレート --> <router-outlet></router-outlet>
これで動かしてみると'http://localhost:4200'のアクセスは'http://localhost:4200/detail/1'にリダイレクトされid=1のユーザが表示されるはずです。idの部分のパラメータを変更することで表示するユーザが切り替わることが確認できます。
別コンポーネントでも同一のサービスを使ってみる
別コンポーネントで同一のサービスを利用し2way-bindingによりサービス館でデータが共有されていることを確認します。 まず今回使用するサービスに以下のメソッドを追加します。先に追加している非同期の処理でも大丈夫ですが今回は既にサービスコンポーネントで保有されているデータを返す処理を追加したく非同期である必要はなさそうなのでそれ用のメソッドを追加しています。
// データ共有をするだけの用途とかでPromiseを使わないこともできる getSyncHero(id: number): Hero { return HEROES.find(hero => hero.id === id); }
それから、以下のように既存のindex.component.tsでサービスを利用するように変更します。 index.component.ts
import { Component, OnInit } from '@angular/core'; import { Hero } from 'app/model/Hero'; import { HeroService } from 'app/service/hero.service'; @Component({ selector: 'app-root', templateUrl: './index.component.html', styleUrls: ['./index.component.css'] }) export class AppComponent implements OnInit { textInput = "input test"; hero: Hero = new Hero(); constructor( private heroService: HeroService) { } ngOnInit(): void { this.hero = this.heroService.getSyncHero(1); } }
index.component.html
<!--The whole content below can be removed with the new code.--> <div style="text-align:center"> <h1> <input [(ngModel)]="textInput"><br /> {{ textInput }}<br /> <input [(ngModel)]="hero.name" placeholder="name"><br /> {{ hero.name }} </h1> </div> <!-- ルータ配置用のテンプレート --> <router-outlet></router-outlet>
これでindex.componentにもHeroの情報が表示され、またそれぞれのinputに対して入力すると即時で反映されることが確認できます。
htmlテンプレートでループ処理をする
次にhtmlテンプレート内でループを回して描画を行ってみたいと思います。リストの情報を取得する以下のコンポーネントを追加します。
app/component/heroList/hero.list.component.ts
import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Hero } from 'app/model/Hero'; import { HeroService } from 'app/service/hero.service'; @Component({ selector: 'hero-list', templateUrl: './hero.list.component.html', styleUrls: ['./hero.list.component.css'] }) export class HeroListComponent implements OnInit { heroes: Hero[] = []; title = 'HeroesList'; selectedHero: Hero; // サービスはconstructorに足しておく constructor( private router: Router, private heroService: HeroService) { } ngOnInit(): void { // 再描画のたびに呼ばれるので、ここでメンバ変数を初期化 console.log("HeroListComponent ngOnInit") this.heroService.getHeroes() .then(heroes => this.heroes = heroes); /* this.heroService.getHeroesSlowly() .then(heroes => this.heroes = heroes); */ } onSelect(hero: Hero): void { this.selectedHero = hero; } }
それから、htmlテンプレートを作成します。
<h1>{{title}}</h1> <h2>My Heroes</h2> <ul class="heroes"> <li *ngFor="let hero of heroes" (click)="onSelect(hero)" [class.selected]="hero === selectedHero"> <span class="hero-element"> <span class="badge">{{hero.id}}</span> {{hero.name}}</span> </li> </ul>
上記の<li *ngFor=“let hero of heroes” ~の部分がコンポーネント内のメンバ変数であるheroesをループさせて描画処理を
行っています。hero === selectedHeroの条件が一致している場合はタグのクラスに"selected"を追加します。
それとcssも作成しておきます。
.selected { background-color: #CFD8DC !important; background-color: rgb(0,120,215) !important; color: white; } .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: .5em; height: 1.6em; border-radius: 4px; } .heroes li:hover { color: #607D8B; color: rgb(0,120,215); background-color: #DDD; left: .1em; } .heroes li.selected:hover { /*background-color: #BBD8DC !important;*/ color: white; } .heroes .text { position: relative; top: -3px; } .heroes .badge { display: inline-block; font-size: small; color: white; padding: 0.8em 0.7em 0 0.7em; background-color: #607D8B; background-color: rgb(0,120,215); line-height: 1em; position: relative; left: -1px; top: -4px; height: 1.8em; margin-right: .8em; border-radius: 4px 0 0 4px; } button { font-family: Arial; background-color: #eee; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; cursor: hand; } button:hover { background-color: #cfd8dc; } .error {color:red;} button.delete-button{ float:right; background-color: gray !important; background-color: rgb(216,59,1) !important; color:white; }
あとは、main.moduleに今回のモジュールを追加して、ルータでURLとコンポーネントを関連づけることで表示が行えます。 main.module.ts
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; //テンプレートでバインディングしたり、validationするのに必要 import { AppComponent } from 'index.component'; import { HeroDetailComponent } from 'app/component/heroDetail/hero.detail.component'; import { HeroListComponent } from 'app/component/heroList/hero.list.component'; import { HeroService } from 'app/service/hero.service'; import { AppRoutingModule } from 'app/router/app.router'; @NgModule({ imports: [ AppRoutingModule, // 注意 ルータはdeclationではなくimportsにたす BrowserModule, FormsModule ], declarations: [ AppComponent, HeroDetailComponent, HeroListComponent ], providers: [ HeroService ], bootstrap: [AppComponent] }) export class AppModule { }
app/router/app.router.ts
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { HeroDetailComponent } from 'app/component/heroDetail/hero.detail.component'; import { HeroListComponent } from 'app/component/heroList/hero.list.component'; // コンポーネントとURLを関連づける const routes: Routes = [ { path: '', redirectTo: '/list', pathMatch: 'full' }, { path: 'detail/:id', component: HeroDetailComponent }, { path: 'list', component: HeroListComponent } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
子コンポーネントにデータを渡してみる
サービスを使ってコンポーネント間でデータが共有できるのは確認できましたので、次はサービスを使わずに直接コンポーネントに対してデータが渡せる確認してみたいと思います。
まず、app/component/heroList/hero.list.component.htmlに以下を追加します。
<!-- コンポーネントのメンバ変数を[]で囲ったものに対して選択したheroを渡す --> <hero-detail [hero]="selectedHero" [isSearchMode]="false"></hero-detail>
それからapp/component/heroDetail/hero.detail.component.tsのメンバ変数に@Input()を付与することで親コンポーネントからデータを受け取ることができるようになります。 app/component/heroDetail/hero.detail.component.ts
import { Component, Input, OnInit } from '@angular/core'; import { ActivatedRoute, Params } from '@angular/router'; import { Hero } from 'app/model/Hero'; import { HeroService } from 'app/service/hero.service'; @Component({ selector: 'hero-detail', //ディレクティブのタグ名 templateUrl: './hero.detail.component.html' //htmlテンプレートの読み込み }) export class HeroDetailComponent implements OnInit { // テンプレートhtmlにbindして使用するクラス変数 title = 'HeroDetail'; @Input() hero: Hero = new Hero(); // コンポーネントを使用する側で用途を決めれるようにする @Input() isSearchMode: Boolean = true; constructor( private heroService: HeroService, // urlパラメータを取得するのに必要 private route: ActivatedRoute) { } ngOnInit(): void { if(this.isSearchMode){ // ルータからパラメータ取得 this.route.params.forEach((params: Params) => { console.log("hero detail component ngOnInit"); console.info(params); if (params['id'] !== undefined) { const id = +params['id']; this.heroService.getHeroById(id) .then(hero => this.hero = hero); } }); } } }
コンポーネント間で画面遷移してみる
コンポーネント間で遷移できるようにするためまずapp/component/heroList/hero.list.component.tsに以下のメソッドを追加します。
gotoDetail(): void { this.router.navigate(['/detail', this.selectedHero.id]); }
それから、app/component/heroList/hero.list.component.htmlからgotoDetailを呼び出せるようにするため、以下のように修正します。
<h1>{{title}}</h1> <h2>My Heroes</h2> <ul class="heroes"> <li *ngFor="let hero of heroes" (click)="onSelect(hero)" [class.selected]="hero === selectedHero"> <span class="hero-element"> <span class="badge">{{hero.id}}</span> {{hero.name}}</span> </li> </ul> <!-- コンポーネントのメンバ変数を[]で囲ったものに対して選択したheroを渡す --> <!-- <hero-detail [hero]="selectedHero" [isSearchMode]="false"></hero-detail> --> <div *ngIf="selectedHero"> <h2> {{selectedHero.name | uppercase}} is my hero </h2> <button (click)="gotoDetail()">View Details</button> </div>
次にapp/compnent/heroDetail/hero.detail.compnentでは遷移元に戻れるように以下のメソッドを追加します。
goBack(savedHero: Hero = null): void { window.history.back(); }
それからapp/compnent/heroDetail/hero.detail.compnent.htmlから呼び出せるように以下のように修正します。
<!-- heroが見つかった時のみこの部分を表示する--> <div *ngIf="hero"> <h1>{{title}}</h1> <h2>{{hero.name}} details!</h2> <div><label>id: </label>{{hero.id}}</div> <div> <label>name: </label> <!-- 注意 ngMoelを使う場合はNgModuleでFormsModuleをインポートしないといけない--> <input [(ngModel)]="hero.name" placeholder="name"><br /> <button (click)="goBack()">Back</button> </div> </div> <!-- heroが見つからなかった場合の処理 --> <div *ngIf="!hero"> hero not found. </div>
これでコンポーネント間での画面繊維が確認できたかと思います。
もう一つコンポーネントを追加してみる
次にダッシュボードコンポーネントを追加して、こちらからもhero.detail.compnentに遷移できるようにしたいと思います。 以下のapp/component/dashboard/dashboard.component.tsを作成します。
import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Hero } from 'app/model/Hero'; import { HeroService } from 'app/service/hero.service'; @Component({ selector: 'my-dashboard', templateUrl: './dashboard.component.html', styleUrls: ['./dashboard.component.css'] }) export class DashboardComponent implements OnInit { heroes: Hero[] = []; constructor( private router: Router, private heroService: HeroService) { } ngOnInit(): void { this.heroService.getHeroes() .then(heroes => this.heroes = heroes.slice(1, 6)); } gotoDetail(hero: Hero): void { const link = ['/detail', hero.id]; this.router.navigate(link); } }
それからhtmlテンプレートを作成します。
<div class="grid grid-pad"> <h3>Top Heroes</h3> <div *ngFor="let hero of heroes" (click)="gotoDetail(hero)" class="col-1-4"> <div class="module hero"> <h4>{{hero.name}}</h4> </div> </div> </div>
あとはmain.module.tsでDashboardComponentを読み込むようにし、ルータに追加しておくと画面が表示されるようになります。
httpリクエストを投げれるようにしてみる
次にhttpリクエストを投げれるようにしてみます。リクエストを受けるWEBサーバを準備するのは面倒なのでangularのモックを利用します。そのためにはmain.module.tsで以下のモジュールをインポートするようにします。
import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; // npm install --save angular-in-memory-web-api
“angular-in-memory-web-api"はangular本体に組み込まれていないので以下のコマンドでインストールしておきます。
npm install –save angular-in-memory-web-api
それから、レスポンスとして返すデータを定義するapp/service/in-memory-data.service.tsを作成します。
// angular-in-memory-web-apiで使うモックのapiの初期データ export class InMemoryDataService { createDb() { const heroes = [ { id: 1, name: 'one' }, { id: 11, name: 'Mr. Nice' }, { id: 12, name: 'Narco' }, { id: 13, name: 'Bombasto' }, { id: 14, name: 'Celeritas' }, { id: 15, name: 'Magneta' }, { id: 16, name: 'RubberMan' }, { id: 17, name: 'Dynama' }, { id: 18, name: 'Dr IQ' }, { id: 19, name: 'Magma' }, { id: 20, name: 'Tornado' } ]; return { heroes }; } }
ここまで済んだらmain.module.tsを以下のように修正しWebAPIのモックを使用できるようにします。
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; //テンプレートでバインディングしたり、validationするのに必要 import { HttpModule } from '@angular/http'; // httpサービスを利用するのに必要 // 今回はWebAPIのモックを使用する import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; // npm install --save angular-in-memory-web-api import { InMemoryDataService } from 'app/service/in-memory-data.service'; import { AppComponent } from 'index.component'; import { HeroDetailComponent } from 'app/component/heroDetail/hero.detail.component'; import { HeroListComponent } from 'app/component/heroList/hero.list.component'; import { DashboardComponent } from 'app/component/dashboard/dashboard.component'; import { HeroService } from 'app/service/hero.service'; import { AppRoutingModule } from 'app/router/app.router'; @NgModule({ imports: [ AppRoutingModule, // 注意 ルータはdeclationではなくimportsにたす BrowserModule, FormsModule, HttpModule, InMemoryWebApiModule.forRoot(InMemoryDataService) ], declarations: [ AppComponent, HeroDetailComponent, HeroListComponent, DashboardComponent ], providers: [ HeroService ], bootstrap: [AppComponent] }) export class AppModule { }
次にapp/service/hero.service.tsを修正しWebAPI経由でデータを取得するようにします。まず"angular/http"モジュールとrxjsのtoPromiseをインポートします。 angular/httpのレスポンスはrxjsのtoPromiseで非同期で扱うのでrxjsのインポートも必要になります。
import { Headers, Http, Response } from '@angular/http'; import 'rxjs/add/operator/toPromise';
それからコンポーネント内のメッソッドを以下のように修正しWebAPI経由でデータを取得するように変更します。
private heroesUrl = 'api/heroes'; // URL to web api private headers = new Headers({ 'Content-Type': 'application/json' }); constructor(private http: Http) { } getHeroes(): Promise<Hero[]> { console.log("hero service getHeros"); return this.http.get(this.heroesUrl) .toPromise() // jsonのレスポンスを受け取ってHero型の配列に変換する .then(response => response.json().data as Hero[]) .catch(this.handleError); } getHeroById(id: number): Promise<Hero> { const url = `${this.heroesUrl}/${id}`; return this.http.get(url) .toPromise() .then(response => response.json().data as Hero) .catch(this.handleError); } // httpリクエスト失敗時の処理 private handleError(error: any): Promise<any> { console.error('An error occurred', error); return Promise.reject(error.message || error); }
今回はrxjsを使用していますがレスポンスのjsonをHero型の配列に変換するだけなので
.toPromise() .then(response => response.json().data as Hero)
のようになっています。
動かしてみると一覧表示をする際に毎回データを撮り直しているため、heroの名前を変更して一覧に戻ると変更が反映されないというのが確認できるかと思います。これまではサービスをコンポーネント間でのデータの共有として使っていたのですが、今回の修正でサービスをWebサーバに対してサービスを投げる用途で使うようにしたのでその違いはわかるようにしておきたいです。例えば共通のAPIで取得した結果を複数のコンポーネントで使うという必要があるのでしたら、サービスコンポーネント内にデータ保有用の変数を用意しておきWebAPIを呼び出した後はその変数を変更するようにする必要があるかと思います。
追加、更新、削除のリクエストを投げれるようにしてみる
サービス側に追加、更新、削除のリクエストを投げるメソッドを追加します。
// angular-in-memory-web-apiのcreateApi呼び出し create(name: string): Promise<Hero> { return this.http .post(this.heroesUrl, JSON.stringify({ name: name }), { headers: this.headers }) .toPromise() .then(res => res.json().data as Hero) .catch(this.handleError); } // angular-in-memory-web-apiのupdateApi呼び出し update(hero: Hero): Promise<Hero> { const url = `${this.heroesUrl}/${hero.id}`; return this.http .put(url, JSON.stringify(hero), { headers: this.headers }) .toPromise() .then(() => hero) .catch(this.handleError); } delete(id: number): Promise<void> { const url = `${this.heroesUrl}/${id}`; return this.http.delete(url, { headers: this.headers }) .toPromise() .then(() => null) .catch(this.handleError); }
httpモジュールのpost、put、deleteを使い分けていますが違いはここのstackoverflowを確認するのが良さそうです。
あとは各コンポーネントからサービスを利用するようにしたら、heroの名前変更が一覧に反映されたり、登録、削除が確認できるかと思います。
検索を行ってみる
サービスに検索用のリクエストを投げるメソッドを追加します。
search(term: string): Observable<Hero[]> { return this.http .get(`app/heroes/?name=${term}`) .map(response => response.json().data as Hero[]); }
レスポンスをそのままHero型の配列にセットするだけなので今までと同様にtoPromise()~で大丈夫かと思ったのですが、getリクエストの場合はtoPromiseが使えないようです。 この辺りはrxjsとhttpモジュール周りの学習が必要になりそうです。 それから以下のapp/component/heroSearch/hero.search.componentモジュールを追加してdashboardコンポーネントに配置すると検索コンポーネントが使えるようになります。 app/component/search/hero.search.component.ts
import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import { Subject } from 'rxjs/Subject'; // Observable class extensions import 'rxjs/add/observable/of'; // Observable operators import 'rxjs/add/operator/catch'; import 'rxjs/add/operator/debounceTime'; import 'rxjs/add/operator/distinctUntilChanged'; import 'rxjs/add/operator/switchMap'; import { HeroService } from 'app/service/hero.service'; import { Hero } from 'app/model/Hero'; @Component({ selector: 'hero-search', templateUrl: './hero.search.component.html', styleUrls: [ './hero.search.component.css' ], providers: [HeroService] }) export class HeroSearchComponent implements OnInit { heroes: Observable<Hero[]>; private searchTerms = new Subject<string>(); constructor( private heroService: HeroService, private router: Router) {} // Push a search term into the observable stream. search(term: string): void { this.searchTerms.next(term); } ngOnInit(): void { this.heroes = this.searchTerms .debounceTime(300) // wait 300ms after each keystroke before considering the term .distinctUntilChanged() // ignore if next search term is same as previous .switchMap(term => term // switch to new observable each time the term changes // return the http search observable ? this.heroService.search(term) // or the observable of empty heroes if there was no search term : Observable.of<Hero[]>([])) .catch(error => { // TODO: add real error handling console.log(error); return Observable.of<Hero[]>([]); }); } gotoDetail(hero: Hero): void { let link = ['/detail', hero.id]; this.router.navigate(link); } }
app/component/heroSearch/hero.search.comonent.html
<div id="search-component"> <h4>Hero Search</h4> <input #searchBox id="search-box" (keyup)="search(searchBox.value)" /> <div> <div *ngFor="let hero of heroes | async" (click)="gotoDetail(hero)" class="search-result" > {{hero.name}} </div> </div> </div>
app/component/heroSearch/hero.search.component.css
.search-result{ border-bottom: 1px solid gray; border-left: 1px solid gray; border-right: 1px solid gray; width:195px; height: 16px; padding: 5px; background-color: white; cursor: pointer; } .search-result:hover { color: #eee; background-color: #607D8B; } #search-box{ width: 200px; height: 20px; }
検索用のコンポーネントでは入力に変更があったらrxjsでサービスを呼び出すようにしています。angularのチュートリアルでrxjsが使われているので、angularをやるならrxjsを覚えていた方が良さそうに思いました。 あとは今までと同様main.module.tsでモジュールを読み込むようにしダッシュボードコンポーネントに表示するようにして動作は確認できるかと思います。
触ってみた感想
Angularの1は触ったことがあったのですが、それに比べてだいぶ分かりやすくて扱いやすくなったと思います。特にルータ周りはReactと比べて優位に立ってそうな気がしました。
JavaScriptフレームワーク調査
JavaScriptのモダンフレームワークでどれを選べば良いのか調査した内容になります。
AngularJS
googleによっって開発されているMVVMフレームワーク 現在Angular4まで出ている。出た当初は仮想DOMを使っているReactなどに比べると遅いと言われていたけど、現在はReactと比べて特に遅いというふうではないらしい。
テンプレート
HTMLテンプレートを作成しデータとtwo-wayバインディングさせる。ReactやVueなどの仮想DOMを使うフレームワークの場合はone-wayでデータをバインディングさせるが、Angularの場合は仮想DOMを使っていないためかtwo-wayバインディングで表示用のデータを手軽に操作することができる。 JavaScript内部にhtmlを記述してデータのバインドやテンプレートの読み込みをするなどAngularフレームワーク独特の学習は必要になりそう。
DI
作成したモジュールをサービスとして別のモジュールで利用する(DI)ことができる。コンポーネント間でのデータのやり取りでもサービスを使うらしい。
テスト
Angularのテンプレート構文はJSXのように静的チェックが行えないためテストツールが必須になっているとのことらしい。その分公式の方がどのテストフレームワークを使えば良いか説明していて、それに合わせて作っていたら問題なさそう。
所感
フルスタックフレームワークな分覚えることは多いけど、公式のサポートが充実してそうなので特に困ると言ったことはなさそうなきがする。他のReactやVueなどのフレームワークに比べるとHttpリクエストなどフレームワーク側でのサポートが広い範囲で行われるので安心して使うことはできそう。業務でSPAを扱うと言ったことがあればAngularが良さそうなきがする。
React
React自体はMVVMのViewの部分だけを扱うフレームワークでシンプルと言われているけど、Facebookが提唱するFluxやReduxのアーキテクチャに合わせて実装しようとするのであればその分書くコードは増えて複雑になる。React自体がシンプルな分どう使うかは開発者自身に委ねられている部分が多い気がする。学習コストについてReact自信を覚えるのは大変ではないけどどう設計するかで時間がかかるかもしれない。
テンプレート
テンプレートにはJSXを使用している。JavaScriptの中に直接HTMLを吐きだす関数が書かれているため、デザイナーでない人が見るのは大変だと思われる。JSX自体はReactを使っている開発者自体にも嫌われているので、今後の改善が望まれる。
テスト
公式ではJesとかenzymeが進められていて、Jsetの方が簡単に扱えそうな感じがした。 enzymeのを使用する場合は別でモック用のモジュールを入れている記事がいくつか見受けられる。
Redux
Reduxはだいたいこんな感じだと思う。
アプリケーション全体で管理するstateを変更する場合はマウスクリックなどのイベント後に、状態変更のためのアクションを実行する。
アクションによりデータが作成されたら変更を反映するため、ディスパッチといってstateを管理するstoreにデータを送る。
reduxではここでstateの変更をすぐに変更するのではなくmidlewareの処理を実行する。
midlewareはログの出力やサーバへのリクエスト投げたりするのに使われている気がする。
midlewareが増えると全体的に遅くなるという問題はあるようです。
midlewareの一連の処理が終わったらreducerによりアプリケーションの状態を表すstateが変更され、 そして仮想DOMが変更され描画に反映さる。
所感
Reduxの実装方法であればデータを厳密に扱うことができるので、例えば一つのデータに対していろんな見せ方があったり、いろんなところから変更したりという複雑さがある場合はReactを使った方がよさそうな気がした。
Vue
公式サイトよりVueの概要は以下のようになっています。
Vue.js (発音は / v j u ː /、view と同様) はインタラクティブな Web インタフェースを構築するためのライブラリです。Vue.js のゴールは、 できる限りシンプルな API でリアクティブデータバインディング と 構成可能な View コンポーネントを提供することです。 Vue.js 自体は本格的なフレームワークではありません、Vue.js は View レイヤーだけに焦点を当てています。したがって、Vue.js のいいところだけをピックアップしたり、Vue.js を他のライブラリや既存のプロジェクトに統合することはとても簡単です。一方、Vue.js を適切なツールとサポートするライブラリによる組み合わせで使用する場合、Vue.js は完全に洗練されたシングルページアプリケーションを提供することができます。 あなたが経験豊富なフロントエンド開発者で、 Vue.js を他のライブラリ/フレームワークと比較したい場合、他のフレームワークとの比較を チェックしてください。Vue.js で大規模アプリケーションを扱う方法に興味がある場合は、大規模アプリケーションの構築をチェックしてください。
AngularとReactに比べると学習コストは低く、というか多分Vue自体がjQueryの代替として考えられている気がしてその分低いと認識されている気がする。
テンプレート
htmlのテンプレート構文を利用していてバインドしているデータに変更があったら最小限のDOM再描画を行っている。 テンプレート構文を利用する点がPolymerと似ていたりして、JSXとに比べて普段htmlを 書いている人からしたら親しみやすいのかと思う。オプションでJSXを利用することもできる。
ReactとVueは似ているけど描画のロジックは根本的に違っているようで、Reactの方は際の DOM がどのような状態にするためにメモリ内の表現で仮想 DOM を活用し、状態を変更するとき、React は仮想 DOM の完全な再レンダリングを行い、その差分を求めて、そして実際の DOM にパッチする。 仮想 DOM の代わりに、Vue.js はテンプレートとして実在する DOM を使用し、データバインディングに対して実在するノードに参照を保ちます。そのためかVue.jsは性能のチューニングをほとんど必要としないらしい。
テスト
公式はKarmaを使うことを勧めている。
所感
jQueryの代替品を探しているのであればこれがベストなきがする。
結論
一般的な業務でSPAを扱うということがあるのであれば公式のサポートが手厚いAngularがよさそうに思います。jQueryの代替品を探しているレベルであればVueで手軽に実装するのが良さそう。データ操作周りで複雑なことをして見せたいページを作りたいのであればReactが良さそうな感じでしょうか。
DDD概要
社内の勉強会に備えてのメモ
DDDとは何か?
エリック・エヴァンスがソフトウェアのドメインモデリングと設計についてオブジェクトコミュニティの底流として現れた哲学をドメイン駆動設計と呼称し書籍にまとめた。
DDD(Domain-Driven Design) とはどのように設計していくかという理論、考え方、概念を指している。
具体的な設計・開発方法を指して言って入るわけではないのでわかりづらい点も多々あるかもしれない。
エヴァンス本の参考文献からもDDDは以下の書籍によって支えられて入ることがわかる
・XPエクストリープ プログラミング入門
・テスト駆動開発入門
・アナリシスパターン
・実践UML
・オブジェクト指向入門
他にもたくさんあって、DDDはこれらの書籍によって支えられている。
DDD(エヴァンス本)の目次
エヴァンス本の目次一覧をまとめてみる
第1部 ドメインモデルを機能させる
・第1章 知識をかみくだく
・第2章 コミュニケーションと言語の使い方
・第3章 モデルと実装を結びつける
第1章では設計者とドメインエキスパートでのコミュニケーションによりどうやって深いモデルを見つけていくかについて書かれている。そのためにもドキュメントを作ったり言葉を統一することによりコミュニケーションを取る方法を提示している。第3章では知識をかみくだいてえたモデルをコーディングに落とし込む手法としてモデル駆動設計に触れている。
第2部 モデル駆動設計の構成要素
・第4章 ドメインを隔離する
・第5章 ソフトウェアで表現されたモデル
・第6章 ドメインオブジェクトのライフサイクル
・第7章 言語を使用する:応用例
第2部ではモデルを実装に落とし込むモデル駆動設計について述べている。ドメインの設計をソフトウェアシステムにおけるその他の関心ごとから分離することで設計とモデルとのつながりを明確にする目的がある。そのためにも"第4章ドメインを隔離する"ではUI層、アプリケーション層、ドメイン層、インフラストラクチャ層のレイヤ化アーキテクチャについて説明している。"第5章ソフトウェアで表現されたモデル"ではモデルの実装部分であるドメイン層の構成要素のエンティティ、値オブジェクト、サービスについて説明している。それから2つのモデルを適切に分割(モジュール化)することによるメリットを述べている。"第6章ドメインオブジェクトのライフサイクル"ではエンティティ、値オブジェクトの集約について述べている。例えば自動車のドメインであったら集約ルートが自動車でそれに紐づいてタイヤエンティティ、車輪エンティティ、位置値オブジェクトが存在する。集約全体のオブジェクト精製方法についてファクトリパターンの説明がある。集約のルートがオブジェクトの永続化と永続化されたオブジェクトの検索に使用するリポジトリにも触れている。
第2部は実装寄りの話でここを理解できていればDDDっぽい実装ができるようになりそう。
第3部 より深い洞察へ向かうリファクタリング
・第8章 ブレイクスルー
・第9章 暗黙的な概念を明示する
・第10章 しなやかな設計
・第11章 アナリシスパターンを適用する
・第12章 デザインパターンをモデルに関係づける
・第13章 より深い洞察へ向かうリファクタリング
第2部ではモデルを実装を一致させる基礎の方法を学び、第3部ではドメインについてさらに深いモデルを開発するために必要不可欠となるリファクタリングを学ぶ。"第8章ブレイクスルー"は最初に思い描いたモデルで開発を進めた後、モデル自体は悪くなさそうであっても不具合が生じることがあって、そういうのは普段からの小さいリファクタリングによりモデルを明確化することである時問題が明確化して解決の糸口になるという話。"第9章暗黙的な概念を明示的にする"ではブレイクスルーを引き起こすための日々のリファクタリングとしてそれまで気づいていなか明白でない概念を見つける方法について書いている。"第10章しなやかな設計"では変更に強いしなやか設計はどうすれば行えるかについて書いてある。第11章と第12章ではアナリシスパターンとデザインパターンの利用方法について書いてある。"第13章より深い洞察へ向かうリファクタリング"ではどこからリファクタリングを始めるかについて書いてある。
第4部 戦略的設計
・第14章 モデルの生合成を維持する
・第15章 蒸留
・第16章 大規模な構造
・第17章 戦略をまとめ上げる
第4部ではモデルの生合成を維持していくための戦略的設計について述べている。"第14章モデルの生合成を維持する"では、コンテキストマップを描きコンテキストを境界づけておき、コンテキスト間の関係を明示的にしておく。"第15章蒸留"ではコンテキストマップを描いてコンテキスト間を境界づける際に各ドメインの役割となるコアドメインや汎用サブドメインなどの種類を学ぶ。"第16章大規模な構造"では巨大なシステムに包括的な原則を持たせることで設計全体にまたがるパターンにおいてどのような役割を果たすかという観点を分かりやすくする方法を学ぶ。
全17章でソフトウェアの上流部分からリファクタリング、成長までをまとめた長編になって入るのがわかる。DDD自体は具体的な実装についてあまり触れられていないので、実装にどう活かすかを学びたいのであれば実践ドメイン駆動設計を読んだり、サンプルのプロジェクトを調べるのが良さそう
JJUG_CCC 2017 Spring
2017/05/20(土)に行われたJJUG(ジェイジャグ)に参加してきました。JJUGは毎年2回春と秋に行われていて、今回で20回目とのことでした。1コマ45分のセッションが複数のルームで同時に行われ参加者はどれに出るか自由に選ぶというものになっています。参加者は前回の約700名からさらに増えて1000名近くいたようでとにかくたくさん人がいました。人気のセッションはルームに入る前に行列ができてすぐに満席になっていました。会場の整備や入場規制、タイムキーパーなどで対応いただいた運営の方は忙しい中ありがとうございました。
エンプラ開発におけるレガシーアプリケーションの巻き取りとモジュール分割の戦い
https://www.slideshare.net/KazuhiroWada/2017spring-jjug-cccf2-76144077
既存のレガシーアプリケーションを巻き取ることになったベンダーさんがどうやって品質の改善に取り組んだのかという話でした。元々struts1で作られたアプリがあってそれと連携するアプリを開発して2つのアプリをearで固めてデプロイするということをやっていたらしいです。元々のアプリをアプリ1とし、新しく作るアプリをアプリ2としたら、連携するにあたりアプリ1側で持っていた認可判定の処理をアプリ2に持って行きspring security OAuth2のOAuth2 Providerという機能を利用してアプリ1から認可判定の処理を行うときもアプリ2側に作ったロジックを利用するとのことでした。目標が決まった後はクリスさんのレガシーソフトウェア改善ガイドのような手順で進めていたようです。
Gitリポジトリの管理にはBitbucketを情報共有やバグトラッキングにはAtlassianのConfluenceとJIRAを利用し継続的インテグレーションにはBambooを利用したとのことです。ビルドツールがgradleならGradleWrapperを利用することもできるのでjenkinsでも良さそうな気がしましたが、Bambooは触ったことがないからわからないです。リファクタリング自体で工数が取れることはないので、機能追加とかのタイミングの修正タイミングで少しづつ変えていくしかないのかなと思いました。作業を効率化するにも普段から意識すべきことが聞けてよかったと思います。
Scala製機械学習基盤PredictionIOとSparkによるレコメンドシステム
https://speakerdeck.com/takahiro/building-a-recommendation-engine-with-spark-and-apache-predictionio
PredictionIOのコミッタの方が発表していました。PredictionIOはGithubのScala製OSSでStar数がApacheSparkについで2番目とのことらしいです。SparkやPredictionIOのstar数が多いあたりScalaにはデータの集計や分析の需要が多いのでしょうか。開発元が2016年にSalesForceに買収されたニュースで知った人もいるかと思います。PredectionIOは機械学習に必要となる機能を体型的に備えているため、Datastoreや集計、機械学習、APIサーバとだいぶ機能が豊富な印象です。
発表では求人広告のデータを学習させてオススメの求人を見つけれるようにするための方法について話していました。手順については学習データを用意して前処理を行いモデルを学習しテストを行い、交差検証で一番よかったモデルを採用するというものです。PredictonIO導入前はElasticsearch連携用にインデックスを張ったり学習結果をバルクファイルで出力したり実行フローのスクリプトが属人化して最初に作った人もよくわからない状態であったりなどしたらしいですが、PredictionIO導入により改善が行えたようです。Sparkが流行った流れでPredictionIOにもブームが来るんじゃないでしょうか。
javascriptでオブジェクトのプロパティを再起的にマージしてみる
以外と調べても良いのが見つからなかったから自分で作ってみた時の備忘録
配列とかの順番で情報の持ち方が決まっているのであればこれで良いのかも
function mergeProperty(obj1, obj2){ if(typeof obj2 === "string" ){ return; } if(obj2.length !== void 0 && obj2.length > 0){ for(var i = 0; i < obj2.length; i++ ){ if(obj1.length <= i ){ obj1.push(obj2[i]); }else{ mergeProperty(obj1[i], obj2[i]); } } }else{ for(prop in obj2){ if(obj1.hasOwnProperty(prop)){ mergeProperty(obj1[prop], obj2[prop]); }else{ obj1[prop] = obj2[prop]; } } } }; var obj1 = { prop1: 'obj1-prop1', prop2: [{prop2_1: 'obj1-prop21', prop2_2: 'obj1-prop22'}, {a: 'obj1-prop2_a'}] , prop3: 'obj1-prop3' }; var obj2 = { prop1: 'obj2-prop1', prop2: [{prop2_1: 'obj2-prop21', prop2_2: 'obj2-prop22'}, {b: 'obj2-prop2_b'}, { 3: 'obj2-prop2_3'}] , prop_d: 'obj2-prop_d', prop_e: ['a', 'b'] }; mergeProperty(obj1, obj2); JSON.stringify(obj1); "{"prop1":"obj1-prop1","prop2":[{"prop2_1":"obj1-prop21","prop2_2":"obj1-prop22"},{"a":"obj1-prop2_a","b":"obj2-prop2_b"},{"3":"obj2-prop2_3"}],"prop3":"obj1-prop3","prop_d":"obj2-prop_d","prop_e":["a","b"]}"
こんな感じでobj1にないプロパティがobj2にある場合にのみマージするようになります。
TypescriptでReactのハンズオンプロジェクトを作ってみた
以前ES6でReactのハンズオンを作成し、今回はTypescriptが触ってみたかったのでTypescriptでReactのハンズオンプロジェクトを作ってみました。
まだまだTypescriptに慣れていないので型を指定するところではanyでやり過ごす部分が多々あったので、この辺りは触りながら都度修正していけたらと思います。