Who are U
-
Name
: こざけ しんいち -
Age
: プログラマ定年組 -
Job
: System Architect -
Skill
: Java地方から来ました -
Like
: ダンまち・夏目友人帳・ドラクエ
Have you used Ionic?
Have you used Angular?
Ionicと私
- AngularとSpring Bootを用いた大規模プロジェクトの技術リーダー
- Ionicは画面ルーティングの部分で使用
- 去年4月から開発を進めて今は統合テスト中
- メンバーは多い時で30人くらい
- 3システムで画面は200くらい
ぶっちゃけ疲れました。
IonicとAngular
AngularとIonicに対する個人的な所感です。
AngularのみでWebアプリケーションを作成する場合、色々と下準備が必要ですが、Ionicはアプリケーション作成に必要な部品が一式揃っています。モバイル向けのWebアプリケーションを素早く構築するのであれば、Ionicが向いてると思います。
個人的な所感 | |
---|---|
Angular | コンポーネント指向でSPAを作るための開発キット |
Ionic | Angularでモバイル向けWebアプリケーションを作成するためのアプリケーション基盤 |
Ionicを使いこなすには
「AngularもIonicも学ばないといけないから大変」ということは、ありません。Angularをきちんと学んでおけば、Ionicで新たに学ぶことはあまりないと思います。次の図は、AngularのサイトにあるAngularのアーキテクチャ構造を表した図です。Angularには、コンポーネント、ルーター、DI(Dependency Injection)、モジュール機構など、様々な機能を幅広く学ぶ必要があります。今日はIonicに触れながら、Angularの機能をひとめぐりしたいと思います。

IonicでAngularを学ぼう!
Github
この資料のソースコードはこちらにあります。
Let's start!
では、初めてみましょう。Ionicを始めるのは簡単です。次のコマンドをコンソールで実行します。これで、Webアプリケーションの雛形が完成しました。
npm install -g ionic
ionic start my-awesome-ionic-app blank
ionic serve
早速動かしてみましょう!
ionic serve
コマンドを打つと、開発用サーバーが起動します。
ionic serve
blank画面
http://localhost:8100/home にアクセスすると、次画面が表示されます。

HomePageコンポーネント
こちらが表示されている画面のコードです。
@Component({
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'],
})
export class HomePage {
}
<ion-header>
<ion-toolbar>
<ion-title>
Ionic Blank
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content padding>
The world is your oyster.
<p>If you get lost, the <a target="_blank" rel="noopener" href="https://ionicframework.com/docs/">docs</a> will be your guide.</p>
</ion-content>
コンポーネント
コンポーネントは、テンプレートを使用してコンポーネントのビューを定義します。
テンプレートは、コンポーネントのレンダリング方法をAngularに伝えるHTMLの形式の1つです。

コンポーネントツリー
出来上がった画面は、次の図のコンポーネントツリーを形成しています。
Ionicでは、ページと呼ばれるコンポーネントで1つの画面を表します。雛形のアプリケーションでは、最初にHomePageが表示されます。

こんにちは、世界!
では、画面の表示を変えてみましょう!技術者の世界では、新しいことを学ぶ最初は「Hello world!」と決まっています。画面に「Hello world!」を表示させてみましょう。
<ion-content padding>
Hello {{name}}!
</ion-content>
export class HomePage {
name = 'world';
}
修正すると、このような画面が表示されます。
name
の部分が「world」に置き換えられていますね。

Interpoeration(補間)構文
データバインディングで
name
の部分を「world」に置き換えることが出来ました。データバインディンには様々な記法があり、{{name}}
のような記法はInterpolation(補間)と呼ばれるものです。Interpolationでは、{{1 + 1}}
のような式を書くこともできます。

双方向バインディング
では、Angulaで有名な双方向バインディングで
name
をビューから変更できるようにしてみましょう!次のコードをテンプレートに追加します。
<ion-content padding>
Hello {{name}}!
<ion-item>
<ion-input [(ngModel)]='name'></ion-input>
</ion-item>
</ion-content>
次の画面が表示されます。ビューで名前を変更すると、画面表示が即座に変わります。

# 双方向バインディング
ビューの値とコンポーネントの値を双方向に同期させる仕組みを双方向バインディングと呼びます。次の図は、双方向バインディングの仕組みを表したものです。ビューからコンポーネントへは、イベントで変更通知を行います。Angularは、なんらかの非同期イベント(入力イベントやsetTimeoutなど)を契機に、変更検知と呼ばれる仕組みでコンポーネントの値をビューに反映します。

JavaScriptモジュールとNgModule
Ionicは画面単位でモジュールを作成します。モジュールは混乱しやすい部分ですので、少しモジュールの話をします。
Angularはモジュールという単位でアプリケーションを管理します。すべてのアプリケーションには、少なくとも1つのAngularモジュール(アプリケーションを起動するためにブートストラップするルートモジュール)があります。JavaScriptモジュールとは異なる概念なので注意が必要です。
説明 | |
---|---|
JavaScriptモジュール | JavaScriptコードが含まれる単一のファイル。 |
NgModule |
@NgModule デコレーターで装飾されたクラス。 |
JavaScriptモジュール
JavaScriptモジュールは、JavaScriptコードが含まれる単一のファイルです。
exportされたクラスや関数以外は、外部に公開されません。
JavaScriptモジュールを利用することで、グローバル変数名の競合を防ぐのに役立ちます。
export class User { ... }
function printLog { ... } // export宣言がないので、この関数は外部に公開されない
他のファイルからその機能が必要になった時は次のようにインポートします。
import { User } from './user';
NgModule
NgModuleは@NgModuleデコレーターで装飾されたクラスです。NgModuleは、コンポーネントの定義やそのコンポーネントに必要なモジュール、外部に公開するコンポーネントの指定などをします。

NgModuleには、他にもサービスの提供方法の定義などもありますが、ここでは説明を割愛します。詳細は公式ドキュメントを確認してくださいね。
HomePageModule
Ionicは画面単位でモジュールを作成します。画面ごとにモジュールを作ることで、必要に応じて画面を遅延ロード(lazy load)することで、初回起動の描画速度を高めています。次の図が、HomePageのモジュール構成です。

appCommonモジュールの作成
Ionicでコンポーネントを作成する場合、モジュールでそれを宣言する必要があります。コンポーネントは必ず1つのモジュールのみで宣言しないといけないので、複数画面で使うコンポーネントを作るには、それを宣言するモジュールも作成する必要があります。次のコマンドでアプリケーションで共通に使用するコンポーネントなどを宣言するモジュールを作成します。
ionic generate module modules/appCommon
Beerコンポーネントの作成
共通のコンポーネントを作成する準備が整いました。Beerコンポーネントを作成しましょう!次のコマンドでコンポーネントの雛形を作成します。
ionic generate component components/beer
appCommonモジュールのdeclarationsでBeerコンポーネントを宣言します。また、exportsでこのコンポーネントを外部に公開することを示します。
@NgModule({
declarations: [
BeerComponent
],
imports: [
CommonModule
],
exports: [
BeerComponent
]
})
export class AppCommonModule { }
ホーム画面でBeerコンポーネントを使うために、HomePageModuleのimportにAppCommonModuleを追加します。これにより、AppCommonModuleでexports宣言したBeerコンポーネントをホーム画面で使えるようになります。
@NgModule({
imports: [
AppCommonModule,
CommonModule,
:
これが変更後の構成図です。

では、ホーム画面のテンプレートにBeerコンポーネントを追加してみましょう。
<app-beer></app-beer>
を追加します。
<ion-content padding>
Hello {{name}}!
<ion-item>
<ion-input [(ngModel)]='name'></ion-input>
</ion-item>
<app-beer></app-beer>
</ion-content>
Beerコンポーネントのビューが画面に表示されました!

プロパティバインディング
では、Beerコンポーネントを用いて、プロパティバインディングを試してみましょう。Beerクラスを作成し、その内容をBeerコンポーネントで表示してみます。まずは、次のコマンドでBeerクラスを作成します。
ionic generate class models/beer
Beerクラスを次のように修正します。
export class Beer {
constructor(
public id: string,
public name: string,
public description: string
) {}
}
作成したBeerクラスをBeerコンポーネントにプロパティとして定義します。その際、
@Input
デコレータで修飾することで、このコンポーネントがプロパティバインディング出来ることを宣言します。
export class BeerComponent implements OnInit {
@Input()
beer: Beer;
Beerコンポーネントのテンプレートを次のように修正します。
<p>
{{beer.name}} is {{beer.description}}!!
</p>
ホーム画面で好きなビールの説明を定義して、
export class HomePage {
name = 'world';
myFavoriteBeer = new Beer('1', 'malt\'s', '最高だ');
}
Beerコンポーネントにプロパティバインディングします。
<ion-content padding>
Hello {{name}}!
<ion-item>
<ion-input [(ngModel)]='name'></ion-input>
</ion-item>
<app-beer [beer]='myFavoriteBeer'></app-beer>
</ion-content>
画面にビールの説明が表示されました!

次の図のようなイメージで、画面に表示されています。

イベントバインディング
Beerコンポーネントに「いいね!」ボタンを追加しましょう!ボタンが押された時に、
(click)="onNice()"
のようにテンプレートに記述することで、コンポーネントでイベントを受け取れます。
<p>
{{beer.name}} is {{beer.description}}!!
</p>
<ion-button (click)="onNice()">いいね!</ion-button>
「いいね!」されると嬉しいので、喜びましょう!
export class BeerComponent implements OnInit {
:
onNice() {
this.beer.description += '(((ο(*゚▽゚*)ο)))';
}
}
ボタンにIonicで宣言されたボタンコンポーネントを使いましたので、AppCommonモジュールでIonicModuleをimportしましょう。
@NgModule({
:
imports: [
CommonModule,
IonicModule
],
:
})
export class AppCommonModule { }
ボタンをおすと、喜びました!いいね!

データバインディング
Angularのバインディングは方向によって記法が異なります。双方向については、入力
[...]
と出力(...)
の両方の記法が合わさった記法[(...)]
と考えると、分かりやすいと思います。

構造ディレクティブ - ngIf
「いいね!」を何度も押すと大変なことになりました。

「いいね!」を取り消したいだけなのに!よくないね!
2度押しで「いいね!」を取り消すことができるようにしましょう。
ビューの表示を切り替えるには、構造ディレクティブを使います。ここでは、条件により表示を変えるngIfディレクティブを用います。
Beerクラスに「いいね!」されたかどうかの条件を加えます。
export class Beer {
constructor(
:
public isNice: boolean
) {}
}
最初は、「いいね!」されていない状態として修正します。
export class HomePage {
name = 'world';
myFavoriteBeer = new Beer('1', 'malt\'s', '最高だ', false);
}
「いいね!」ボタンを押されると、ビールの状態が切り替わるように修正して
export class BeerComponent implements OnInit {
:
onNice() {
this.beer.isNice = !this.beer.isNice;
}
}
ngIfディレクティブで条件により表示が切り替わるように修正します。これで、「いいね!」の取り消しができるようになりました!
<p>
{{beer.name}} is {{beer.description}}
<span *ngIf="beer.isNice">(((ο(*゚▽゚*)ο)))</span>!!
</p>
ディレクティブ
「いいね!」が押されてるのに、いまいち表示が地味です。ディレクティブを使ってもっと派手に表示できるようにしましょう!ディレクティブを用いることで、要素やコンポーネントの見た目や動作を変更できます。
次のコマンドでディレクティブを作成します。
ionic generate directive directives/highlight
AppModuleにHighlightDirectiveの宣言が追加されているので、それを削除し、AppCommonModuleで宣言してください。
@NgModule({
declarations: [
BeerComponent,
HighlightDirective
],
:
exports: [
BeerComponent,
HighlightDirective
]
})
export class AppCommonModule { }
自分が宣言された要素(エレメント)の参照をコンストラクタで取得し、入力条件により、背景色を変更する処理を加えます。
export class HighlightDirective {
@Input()
set appHighlight(condition: any) {
this.el.nativeElement.style.background = condition ? 'yellow' : null;
}
constructor(private el: ElementRef) {}
}
p要素に作成したディレクティブを追加して、「いいね!」されたかどうかの条件を渡してあげます。
<p [appHighlight]="beer.isNice">
{{beer.name}} is {{beer.description}}
:
「いいね!」すると、派手に喜ぶ感じになりました!

パイプ
「malt's」ってなんですかね!「MALT'S」ですよね!
パイプを使って、ビール名の表示は大文字になるようにしましょう!
次のコマンドでupperパイプを作成します。
ionic generate pipe pipes/upper
AppModuleにUpperPipeの宣言が追加されているので、それを削除し、AppCommonModuleで宣言してください。
@NgModule({
declarations: [
BeerComponent,
HighlightDirective,
UpperPipe
],
:
exports: [
BeerComponent,
HighlightDirective,
UpperPipe
]
})
export class AppCommonModule { }
パイプは入力値を変換する関数のような機能を提供します。transformメソッドで入力値を大文字に変更するよう処理を加えます。
export class UpperPipe implements PipeTransform {
transform(value: string, args?: any): any {
return value.toUpperCase();
}
}
beer.name | upper
のように作成したパイプを適用します。使い方はUnixのパイプとそっくりですね!
<p [appHighlight]="beer.isNice">
{{beer.name | upper}} is {{beer.description}}
<span *ngIf="beer.isNice">(((ο(*゚▽゚*)ο)))</span>!!
</p>
<ion-button (click)="onNice()">いいね!</ion-button>
MALT'S!!。やっぱり大文字の方がいいですね!

サービス
もっとBeerを!たくさんのBeerを提供したいです。せっかくだから(なにが)、Beerクラスをサービスから提供できるようにします。Beerサービス、いい言葉です。サービスはただのクラスです。サービスを用いることで、複数の画面で値を共有出来たりします。また、いわゆるビジネスロジックを定義する場所としても妥当だと思います。
次のコマンドでBeerサービスを作成します。
ionic generate service services/beer
Beerサービスを次のように修正します。
@Injectable({
providedIn: 'root'
})
export class BeerService {
beers: Beer[] = [
new Beer('1', 'malts', '最高だ', false)
, new Beer('2', 'kirin', '幸せだ', false)
, new Beer('3', 'dry', '花金だ', false)
];
constructor() { }
getBeers(): Beer[] {
return this.beers;
}
}
ホーム画面をBeerサービスからたくさんのビールを提供してもらえるよう修正します。
ngOnInit()
メソッドはAngularのライフサイクルイベントで、コンポーネント初期化時に呼ばれます。
export class HomePage implements OnInit {
name = 'world';
beers: Beer[];
constructor(
private beerService: BeerService
) {}
ngOnInit() {
this.beers = this.beerService.getBeers();
}
}
テンプレートで繰り返し制御をする場合は、ngForディレクティブを使えます。
:
<ion-content padding>
:
<ng-container *ngFor="let beer of beers">
<app-beer [beer]="beer"></app-beer>
</ng-container>
</ion-content>
たくさんのビールが表示されました!!

画面の作成
では、最後に画面を作成しましょう!Ionicらしい画面にしていきたいと思います。次のコマンドでビール詳細画面を作成します。
ionic generate page pages/beer-detail
ビール詳細画面でBeerコンポーネントを使えるように、BeerDetailPageModuleでAppCommonModuleをimportします。
@NgModule({
imports: [
AppCommonModule,
CommonModule,
:
],
declarations: [BeerDetailPage]
})
export class BeerDetailPageModule {}
ビール詳細画面のルータ定義を変更します。ここでは、
'beer-detail/:id'
でビールのIDをURLから取得できるように変更しました。
const routes: Routes = [
{ path: '', redirectTo: 'home', pathMatch: 'full' },
{ path: 'home', loadChildren: './home/home.module#HomePageModule' },
{ path: 'beer-detail/:id', loadChildren: './pages/beer-detail/beer-detail.module#BeerDetailPageModule' },
];
Beerサービスに、IDでビールを受け取ることができるメソッドを追加して、
export class BeerService {
:
getBeer(id: string): Beer {
return this.beers.find(b => b.id === id);
}
}
ビール詳細ページでは、URLで渡されたIDから表示するビールを取得するようにします。
export class BeerDetailPage implements OnInit {
id: string;
beer: Beer;
constructor(
private route: ActivatedRoute,
private beerService: BeerService) {
this.id = this.route.snapshot.paramMap.get('id');
}
ngOnInit() {
this.beer = this.beerService.getBeer(this.id);
}
}
ビール詳細画面のテンプレートでは、Beerコンポーネントを使った表示と、Beerの名前や説明を修正できるようにして、
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button defaultHref="/home"></ion-back-button>
</ion-buttons>
<ion-title>beer-detail</ion-title>
</ion-toolbar>
</ion-header>
<ion-content padding>
<app-beer [beer]="beer"></app-beer>
<ion-item>
<ion-label>名前</ion-label>
<ion-input [(ngModel)]="beer.name"></ion-input>
</ion-item>
<ion-item>
<ion-label>説明</ion-label>
<ion-input [(ngModel)]="beer.description"></ion-input>
</ion-item>
</ion-content>
ホーム画面の表示を一覧形式に変更します。
routerLink
でビール詳細画面への遷移URLを指定します。
<ion-content padding>
<ion-list>
<ion-item
*ngFor="let beer of beers"
routerLink="/beer-detail/{{ beer.id }}"
routerDirection="forward">
<ion-label [appHighlight]="beer.isNice">
{{beer.name | upper}} is {{beer.description}}
<span *ngIf="beer.isNice">(((ο(*゚▽゚*)ο)))</span>
</ion-label>
</ion-item>
</ion-list>
</ion-content>
ビール一覧画面(ホーム画面)
Ionicライクな画面が出来ました!!(((ο(゚▽゚)ο)))

ビール詳細画面

まとめ
ビールが飲みたくなりましたね。
ご静聴ありがとうございました!