# 下準備
1.2 まで進めてもらえると当日はスムーズです。(DLがほとんどです。) 1GB強あるので有線環境がお勧めです。
# Angular を触ってみよう
# Angular の紹介
- 公式(日本語) => https://angular.jp/ (opens new window)
Angular は2016年に発表されたwebフレームワークで、googleが中心になって開発されています。
前身となるAngularJS(1系)は2009年に登場し、双方向バインディングなど現在のAngularに繋がる多くの概念を生み出しましたが、パフォーマンスや使い勝手の向上を目的にバージョン2.0に上がる際に1から作り直されました。
(現在では「Angular」と呼ぶときはバージョン2以降を、「AngularJS」と呼ぶと1系を指すので、呼び方には少し注意が必要です。)
特徴としては
- フルスタックフレームワーク
- Angular だけでフロントエンド開発に必要な機能が揃っている
- TypeScript ベースで開発されている
- 公式ドキュメントも全て TypeScript ベース
- RxJSを使ったリアクティブプログラミングが基本になっている
- component指向
- Angular CDKによる利用度の高い機能が半公式で提供されている
- 半年に1回のメジャーリリース
などが上げられます。
比較的大規模なWebアプリケーションの構築に向いています。 その理由としては、TypeScriptでの開発が強制されること。 ベストプラクティスな構成が公式から提供されているため、アプリケーションが大きくなってきても破綻しにくい。などが挙げられます。 (大抵の必要なツールや機能が全て公式から提供されているため組み合わせに悩まなくていいのもポイント)
逆に記述量が多いため、小さいアプリケーションの開発ではオーバーヘッドが大きくなりがち。また学習コストも比較的高いです。
# 1: 始めに
Angular でアプリケーションを構築する場合はほぼ必ず angular-cli (opens new window) というツールを利用します。これはAngular専用のCLIツールで、テンプレートコードを生成したり、開発用サーバを立ち上げたりしてくれます。 コマンド名は「ng」です。(aNGularの略称)
まずはこれを使い、自動生成されるAngularアプリケーションを起動してみましょう。
# 1.1: docker imageの利用方法
あらかじめAngularがインストールされたコンテナイメージに、ホストのディレクトリをマウントして開発を進めます。 こうすると、ホスト側で好きなエディタを使えるので開発が楽になります。 ただし、コマンドはdockerのbashで実行する必要があります。
docker pull forestsource/bootcamp-angular
cd <好きなディレクトリ ex. "/var/tmp/angular">
# MacOS, Linux
docker run --name bootcamp-angular -it --rm -v "$(pwd)":/app -p 4200:4200 forestsource/bootcamp-angular bash
# Windows
## Docker Desktopの Settings -> Resources -> FILE SHARING -> C にチェックを入れる(作業したいディレクトリがあるドライブにチェック)
mkdir C:\Users\%username\Desktop\bootcamp-angular
docker run --name bootcamp-angular -it --rm -v C:\Users\%username\Desktop\bootcamp-angular:/app :/app -p 4200:4200 forestsource/bootcamp-angular bash
# コマンド実行用にシェルを起動しておく
docker exec -it bootcamp-angular bash
2
3
4
5
6
7
8
9
10
11
12
13
# 1.2: angular-cliで開発環境を構築
今回使うdocker imageにはすでにangular(angular-cli)がインストールされています。
ng new bootcamp-angular
# > ? Would you like to add Angular routing? (y/N) : y Angularを選択するユースケースではほぼ間違いなく使うかと思います。
# > CSS を選択
# > ✔ Packages installed successfully. と出力されたら成功です。
cd bootcamp-angular/
# Angular アプリケーションが生成されている
ls -l
# アプリ起動
ng serve --host 0.0.0.0
2
3
4
5
6
7
8
9
10
11
12
アプリケーションの起動後 http://localhost:4200 (opens new window) にアクセスするとサンプルアプリケーションが表示されます。 Angular 開発環境の構築はこれで完了です。簡単ですね!
# 2: 基本
# 2.1: タイトル変更
ng new
で生成されたアプリケーションの中身を少し見てみましょう。Angular アプリケーションのソースコードは主に src/app/
以下にあります。
-rw-r--r-- 1 root root 246 Aug 3 10:18 app-routing.module.ts
-rw-r--r-- 1 root root 0 Aug 3 10:18 app.component.css
-rw-r--r-- 1 root root 25757 Aug 3 10:18 app.component.html
-rw-r--r-- 1 root root 1089 Aug 3 10:18 app.component.spec.ts
-rw-r--r-- 1 root root 220 Aug 3 10:18 app.component.ts
-rw-r--r-- 1 root root 393 Aug 3 10:18 app.module.ts
2
3
4
5
6
Angular はcomponent指向のフレームワークという話をしましたが、この.component
とついているのが1つのcomponentです。このapp.component
はアプリケーション全体を束ねる親componentになります。
app.component.html
の中を見ると以下のようなコードがあります。
行が長いのでcat app.component.html | grep "app is running!"
すると見つけられます。
<span>{{ title }} app is running!</span>
{{}}
というhtmlには見慣れない記法が入っています。Angular ではこのようにhtml側に変数を展開しながらUIを作っていきます。title
という変数はapp.component.ts
で宣言されています。
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
title = 'bootcamp-angular';
}
2
3
4
5
6
7
8
9
10
色々書いてありますが、ほとんどはおまじないだと思ってもらえればいいです。重要なのはtitle = 'bootcamp-angular';
で、このようにAppComponent
クラスで宣言した変数がhtml側でとして展開可能です。
試しにタイトルを変更してみましょう。
title = 'my-first-angular';
と変更してファイルを保存してください。ブラウザが勝手に更新されてタイトルが変更されます。
# 2.2: component作成
次はcomponentを作ってみましょう。angular-cliには雛形を自動的に生成してくれる機能があります。以下のコマンドを実行してください。
ng generate component peoples
#> CREATE src/app/peoples/peoples.component.css (0 bytes)
#> CREATE src/app/peoples/peoples.component.html (22 bytes)
#> CREATE src/app/peoples/peoples.component.spec.ts (635 bytes)
#> CREATE src/app/peoples/peoples.component.ts (279 bytes)
#> UPDATE src/app/app.module.ts (479 bytes)
2
3
4
5
6
7
8
するとsrc/app/peoples/
以下にcomponentの雛形が生成されます。ただし作っただけでは表示されません。componentを表示するには以下の2通りの方法があります。
- 既存のcomponentに埋め込んで表示する
- ルーティングを登録して別ページとして表示する
# 2.3: ルーティング作成
ここではルーティングを登録してみましょう。src/app/app-routing.module.ts
を以下のように変更してください。
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { PeoplesComponent } from './peoples/peoples.component';
const routes: Routes = [
{ path: 'peoples', component: PeoplesComponent}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
2
3
4
5
6
7
8
9
10
11
12
13
これは/peoples
というパスにアクセスした時PeoplesComponent
の内容を表示するという設定です。
PeoplesComponent
の表示位置はsrc/app/app.component.html
にある
<router-outlet></router-outlet>
の部分に表示されます。試しに http://192.168.20.10:4200/peoples (opens new window) にアクセスしてみてください。下の方にpeoples works!
が表示されていれば成功です。
このようにAngularなどモダンなフレームワークはページ遷移を再現するための「ルーター」と呼ばれる機能を提供しています。
ついでに邪魔なので、src/app/app.component.html
にはpeoples
へのリンクだけを残して綺麗にしてしまいましょう。
この時 <style>
タグは残しておくと、わずかに見やすくなります。
<h1>Hello Angular!</h1>
<ul>
<li>
<a routerLink="/">top</a>
</li>
<li>
<a routerLink="/peoples">peoples</a>
</li>
</ul>
<router-outlet></router-outlet>
2
3
4
5
6
7
8
9
10
11
12
このように、定義したroutingに従ってa
タグでリンクを張ることができます。
# 3: API からデータを取得してみる
# 3.1: model作成
次は外からHTTPアクセスでデータを取得してみます。実際にはWebサーバのAPIをたたくことが多いですが、今回はjQueryの時と同様に以下のjsonファイルの内容を取得してみます。
https://raw.githubusercontent.com/iij/bootcamp/master/test.json (opens new window)
TypeScriptでは型の分からないobjectを扱うのは基本的には避けるべきです。そこでまずは取得するデータの型を定義しましょう。型を定義するにはinterface
を使います(これはTypeScriptの話)。
angular-cliではinterace
を生成する機能があるので、それを使ってみましょう。
ng generate interface models/people
#> CREATE src/app/models/people.ts (28 bytes)
2
すると src/app/models/people.ts
にファイルが生成されます。取得するデータに合わせてfieldを定義しましょう。
export interface People {
id: number;
name: string;
}
2
3
4
これでid
とname
を持つようなobjectをPeople
型として扱えます。逆に言うとPeople
型なオブジェクトには必ずid
とname
が存在することが保証されています。
続いてHTTPリクエストを実行する部分を書いてみましょう。AngularではHTTPリクエストはService
を使って実行するのが基本です。なぜService
を使うのかなど、詳しくは 公式ドキュメント (opens new window) を参照してください。
早速Service
の雛形を作りたいところですが、もう一つ準備があります。Angular ではmodule
と言う単位でアプリケーションが分離されており、今回のようにHTTPのリクエストを行うためにはHttpClientModule
を追加で読み込む必要があります。
(これは不要なコードを読み込まないようにしてブラウザで動くJavascriptのコードを少しでも減らすための工夫です)
moduleの読み込みはapp.module.ts
で行います。以下のように追記してください。
@@ -1,5 +1,6 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
+import { HttpClientModule } from '@angular/common/http';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@@ -12,7 +13,8 @@
],
imports: [
BrowserModule,
- AppRoutingModule
+ AppRoutingModule,
+ HttpClientModule
],
providers: [],
bootstrap: [AppComponent]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
これでアプリケーションにHttpClientModule
が読み込まれます。ではService
の雛形を作りましょう。
# 3.2: service作成
ng generate service services/people
#> CREATE src/app/services/people.service.spec.ts (357 bytes)
#> CREATE src/app/services/people.service.ts (135 bytes)
2
3
4
5
services/people.service.ts
にファイルが生成されます。テストコードも一緒に生成されますが、今回はテストコードまで触れません。
以下のようにjsonを取得するためのコードを書いていきましょう。この時点ではまだどこからも呼び出されていないので保存してもアプリケーションは更新されません。
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { People } from '../models/people';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class PeopleService {
private jsonUrl = 'https://github.com/iij/bootcamp/test.json';
constructor(private http: HttpClient) { }
getJson(): Observable<People[]> {
return this.http.get<People[]>(this.jsonUrl);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 4: データをHTMLに表示してみる
# 4.1: service呼び出し
上で書いたService
のgetJson
メソッドを実行すると、URL先のjsonを非同期通信で取得します。非同期に取得したデータをHTMLに表示してみましょう。
まずはcomponentでserviceを呼び出すためpeoples/peoples.component.html
を以下のように追記してください。
import { Component, OnInit } from '@angular/core';
import { PeopleService } from '../services/people.service';
import { take } from 'rxjs/operators';
@Component({
selector: 'app-peoples',
templateUrl: './peoples.component.html',
styleUrls: ['./peoples.component.css']
})
export class PeoplesComponent implements OnInit {
peoples = [];
constructor(private peopleService: PeopleService) { }
ngOnInit() {
this.peopleService.getJson().pipe(
take(1)
).subscribe(response => {
this.peoples = response;
});
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
少し解説します。
ngOnInit
: Angular のcomponentが画面に表示されるタイミングで実行される関数です。constructor
が実行されるタイミングはcomponentの描画と無関係なため、初期化処理は基本的にngOnInit
に記述します。
subscribe
: Observable に非同期データが流れて来た時の処理を記述しますPromise
のthen
やcallback関数のようなものです。- Observable について詳しくは「補足」で記述します。
take(1)
: Observable 処理を1回で終了します。- 詳しく語ると長いですが、これをしないと
subscribe
が延々とデータを待ち続けてしまいます。 unsubscribe
する手もありますが、今回はシンプルにいきました。
- 詳しく語ると長いですが、これをしないと
ややこしく見えますが、やってることはjsonを取得して結果をpeoples
という変数に入れているだけです。次にpeoples
の中身を表示するHTML側を書いていきます。
# 4.2: 変数表示
peoples/peoples.component.html
を以下のように編集してください。
<p>
peoples works!
</p>
<table border=1>
<tr><th>ID</th><th>Name</th></tr>
<tr *ngFor="let people of peoples">
<td>{{ people.id }}</td><td>{{ people.name }}</td>
</tr>
</table>
2
3
4
5
6
7
8
9
Angular らしさが出てきました。AngularではHTML上に*ngFor
のような独自の記法がよく登場します。
*ngFor
は Angular の機能の中でもよく使うものです。配列やmapを指定するとfor
文のようにHTMLを展開してくれます。今回はpeoples
と言う配列を渡しているので、peoples
の長さだけ<tr></tr>
が繰り返し表示されます。
繰り返しの中では、各要素にpeople
と言う変数でアクセスできるようにしています。
このようにAngularではJavaScriptで作られた値(今回はpeoples
)を少ないコード量でHTMLに表示することができます。
# 5: Formを作ってみる
最後に簡単な入力フォームを作ってみます。Angular でフォームを作るには以下の2通りのやり方が有ります。
- テンプレート駆動フォーム: AngularJS の頃に近い書き方。
ngModel
で変数をformに紐づける。 - リアクティブフォーム: Observable に親和性の高い書き方。JavaScript側で実際に値を変更したりできる。
詳しい違いは 公式ドキュメント (opens new window) を参照してください。
最近ではテストが容易などの理由でリアクティブフォームを使って書くことが多いですので、ここでもそちらを使っていきたいと思います。
まずはTypeScript側のコードを書いていきます。まずはHTTPリクエストの時と同じようにmoduleを追加しましょう。
app.module.ts
を以下のように変更してください。
@@ -1,6 +1,7 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
+import { ReactiveFormsModule } from '@angular/forms';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@@ -14,7 +15,8 @@
imports: [
BrowserModule,
AppRoutingModule,
- HttpClientModule
+ HttpClientModule,
+ ReactiveFormsModule
],
providers: [],
bootstrap: [AppComponent]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
続いてcomponentを書いていきます。peoples/peoples.component.ts
を以下のように変更してください。
import { Component, OnInit } from '@angular/core';
import { PeopleService } from '../services/people.service';
import { FormControl } from '@angular/forms';
import { take } from 'rxjs/operators';
@Component({
selector: 'app-peoples',
templateUrl: './peoples.component.html',
styleUrls: ['./peoples.component.css']
})
export class PeoplesComponent implements OnInit {
peoples = [];
peopleName = new FormControl('');
constructor(private peopleService: PeopleService) { }
ngOnInit() {
this.peopleService.getJson().pipe(
take(1)
).subscribe(response => {
this.peoples = response;
});
}
addPeople() {
const last = this.peoples[this.peoples.length - 1];
this.peoples.push({
id: Number(last.id) + 1,
name: this.peopleName.value
});
this.peopleName.setValue('');
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
peopleName
という名前でform用の変数を定義します。text inputに入力されたtextはこの変数に入ります。
addPeople()
というメソッドを実行するとpeoples
の末尾に入力した名前のデータが挿入されます。
続いてHTML側を書きましょう。一番下の4行を追加してください。
<p>
peoples works!
</p>
<table border=1>
<tr><th>ID</th><th>Name</th></tr>
<tr *ngFor="let people of peoples">
<td>{{ people.id }}</td><td>{{ people.name }}</td>
</tr>
</table>
<br/>
<input type="text" [formControl]="peopleName"> <button (click)="addPeople()">追加</button>
<br/>
<span *ngIf="peopleName.value.length > 0">入力中: {{ peopleName.value }}</span>
2
3
4
5
6
7
8
9
10
11
12
13
[formControl]
という部分でpeopleName
をinputタグに紐づけます。そしてボタンをクリックした時にaddPeople()
が実行されるようにclick
イベントを定義しました。
何か入力して「追加」ボタンを押すと表示に追加されるはずです。
一番下の行は不要ですが、Angular の機能を紹介するために書いています。*ngIf
は*ngFor
よりもさらに使う機能で、引数内の条件式がtrue
の時にだけhtml要素を表示してくれます。
ここまでくればこのような画面が表示されます。
# 6: 最後に
Angular の機能と基本的な書き方を紹介しました。ここで紹介できたのはほんの一部ですので、興味があれば 公式ドキュメント (opens new window) を参照してください。
# (補足)ユニットテスト
ユニットテスト とは、メソッドや関数単位でテストコードを作成し、コードを書くのと同時に自動でテストを行う開発手法です。TDD (Test Driven Development) など聞いたことがあるかもしれません。
実はng new
コマンドはユニットテスト用の環境も用意してくれています。
# 設定変更
./bootcamp-angular/karma.conf.js(テストツールの設定ファイル)
のブラウザ設定を下記のように必要があります。
Chromeはrootで実行する場合は--no-sandbox
が必要なので追加しています。
@@ -25,7 +25,13 @@
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
- browsers: ['Chrome'],
+ browsers: ['ChromeHeadlessNoSandbox'],
+ customLaunchers: {
+ ChromeHeadlessNoSandbox: {
+ base: 'ChromeHeadless',
+ flags: ['--no-sandbox']
+ }
+ },
singleRun: false,
restartOnFileChange: true
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
テストを実行してみます。
ng test
#> 09 05 2020 16:06:23.410:INFO [karma-server]: Karma v5.0.5 server started at http://0.0.0.0:9876/
#> 09 05 2020 16:06:23.411:INFO [launcher]: Launching browsers ChromeHeadlessNoSandbox with concurrency unlimited
#> 09 05 2020 16:06:23.466:INFO [launcher]: Starting browser ChromeHeadless
#> ...(省略)
#> TOTAL: 2 FAILED, 1 SUCCESS
#> TOTAL: 2 FAILED, 1 SUCCESS
2
3
4
5
6
7
8
ユニットテストが実行され、いくつかエラーが表示されると思います。
これはデフォルトで生成されるテストコードに「タイトルがbootcamp-angular
であること」を確認するテストが含まれているためです。
そのテストコードはsrc/app/app.component.spec.ts
にあります。
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'bootcamp-angular'`, () => {
const app = fixture.componentInstance;
expect(app.title).toEqual('bootcamp-angular');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('.content span').textContent).toContain('bootcamp-angular app is running!');
});
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
いろいろ書いてありますが、今はおまじないだと思ってください。重要なのは上の二ヵ所です。 ここでは以下の2つのテストを実施しています。
AppComponent
クラスのtitle
変数の内容がbootcamp-angular
であること。- レンダリングされたhtml (完成系のhtml)の
span
タグの中にbootcamp-angular app is running!
という文字列が含まれること
コンポーネントを追加・編集したので、テストが失敗するようになっています。
これを編集してテストが通るようにしてみてください。ng serve
と同様に、ng test
もファイルを編集すると自動的にテストをやり直してくれます。
# (補足) Observable
Angularを使う上で避けては通れないのがObservable
です。これはPromise
と同じように非同期処理を解決するためのデザインパターンで、Angular ではHTTPリクエストだけではなく、「全て」の状態変化がRxJS
というライブラリを使ったObservableで管理されています。
Observableパターンでは非同期なデータの流れを「ストリーム」として扱い、そのストリームを変更していくことで非同期データを扱います。ほんの一例ですが、例えば以下のようなことができます。
observable = of(1, 2, 3, 4, 5); // 1~5の数字が順番に流れてくるストリームを作成
observable.pipe(
map(num => { return num * 2 }), // 各値を2倍
filter(num => num > 4), // > 4 な値のみにフィルタリング
).subscribe(num => {
console.log(num); // 「6」「8」「10」 が順番に表示される
});
2
3
4
5
6
7
やはり詳しくは 公式ドキュメント (opens new window) を参照してください(若干分かりにくいかもしれません)。
Angular ではHTTPリクエストはもちろん、URLの変更やform要素の入力、ユーザーイベントなど全ての変更がObservableで処理されています。