5. 本書の補足
5-1. 観光情報検索アプリの実装
5-1-1. アプリ全体の構造

このアプリは3つの層で構成されています。
1層目: アプリ共通の処理(index.htmlファイルとRootComponentクラス)
2層目: 画面ごとの表示(各Componentクラス)
3層目: データ処理(各サービスクラス)
基盤となる1階層目は、index.htmlでロードされるroot.component.tsに実装しています。
5-1-2. ルートコンポーネントの役割
ルートコンポーネントはアプリ全体で共通する処理を担当しています。具体的には、へッダやメニューなどの複数画面で共通する表示、画面の回転やブラウザを閉じるなど任意の画面で発生するイベントの検出を行います。
また、どのコンポーネントよりも先に呼びだされるため、コンポーネント共通の初期化もここで行います。
1
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69 | //ルートコンポーネント
//kankoClient¥src¥app¥component¥root¥root.component.ts
//=================
//初期化処理
//=================
async init() {
//設定情報読み込み
this.stateService.restoreState();
//データサービス初期化
let res = await this.dataService.init();
//非同期の初期化処理完了を待たせるためのフラグ
this.isWait = false;
//検索入力画面へ遷移
this.router.navigate(['/search']);
}
//=================
//メニュー選択時の処理
//=================
//お気に入りの表示
async favorite() {
this.router.navigate(['/favorite']);
}
//条件を変えて検索
search() {
this.router.navigate(['/search']);
}
//新規検索
newSearch() {
this.stateService.state.genres = [];
this.stateService.state.areas = [];
this.stateService.state.selectedKeys = [];
this.router.navigate(['/search']);
}
//設定ダイアログの表示
setting() {
this.bottomSheet.open(SettingComponent);
}
//=================
//共通イベント
//=================
//任意の場所のマウスクリックイベント
clickOp(e: Event) {
if (!this.stateService.state.setting.modern.value) {
console.log('@@@ mouse click');
this.isQue = true;
setTimeout(() => {
this.isQue = false;
}, 2000);
}
}
//ブラウザが閉じるイベント
@HostListener('window:beforeunload', ['$event'])
beforeUnload(e: Event) {
console.log('@@@before unload');
this.stateService.saveState();
}
//スマートフォンで画面の縦横回転
resize(e: Event) {
console.log('@@@ window resize');
this.stateService.publish(MyEvent.RESIZE);
}
|
2、3階層目の処理については次項の「実装のポイント」で解説します。
5-2. データベース検索の高速化
実装のポイント(1)
観光情報検索アプリの場合、従来のサーバー集中型では、以下のタイミングで通信待ちが発生します。
・検索条件入力する毎の結果件数表示(画面右上のxx件の表示)
・検索条件入力から検索結果表示への画面切替
ここでは、これまでサーバーから受信していたデータの事前作成、事前取得、分割取得を行い、サーバーの通信待ちを回避し、画面表示の大幅な高速化を実現します。
5-2-1. 事前データ作成
検索時間を短縮するために、検索結果の件数をサーバーで事前に作成しておきます。アプリ起動時にこのデータをロードすることで検索結果件数の取得にサーバーとの通信が不要になります。それによって、検索条件を変更するたびに、その結果件数を瞬時に表示することが可能になります。
1
2
3
4
5
6
7
8
9
10
11
12
13 | //検索結果件数の事前作成データ
//kankoClient¥src¥app¥app.countTable.ts
//キーは"エリア(1桁)ジャンル(1桁)カテゴリー(2桁)
//バリューは、キーに該当する検索条件の件数
//例えば"1101":6は、北海道(コード:1)、自然景観(コード:1)、
//山岳(コード:01)は、6件の情報があるとわかります。
export const COUNT_TABLE ={
"1101": 6, //北海道、自然景観、山岳は6件
"1102": 1,
"1103": 4,
"1104": 4,
"1105": 10,
|
試しに事前作成データと同じ条件でアプリを操作してみます。
「北海道」、「自然景観」、「山岳」は、6件の情報があると表示され、下図右上の検索結果件数の値が事前作成データと一致します。

5-1-2. 事前読み込み
検索条件入力画面で 条件が確定した時点で、検索結果データをバックグラウンドでサーバーから受信します。検索結果件数を確認して検索結果画面を呼びだす時には、すでにデータを取得していますので即座に表示できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | //ウイザードのステップ変更時の処理
//kankoClient¥src¥app¥component¥search¥search.component.ts
//ウィザード(stepper)が変化するときに呼び出されるメソッド
async onChangeStep(event) {
//表示内容の更新
this.update();
//ウィザード3番目(indexは2)の「該当件数」が呼びだされたタイミングで
//サーバからデータ取得
if (event.selectedIndex === 2) {
if(!this.stateService.state.setting.prefetch.value)return;
//事前読み込みはスクロールサービスにて行う
//kankoClient¥src¥app¥service¥scroll.service.ts
await this.scrollService.prefetch();
}
|
この様子は Google Web developer Tools でネットワークのモニターで確認できます。
検索条件として「北海道」、「自然景観」、「山岳」と選択すると3番目のウィザードが呼びだされたタイミングでサーバーから検索結果データがダウンロードされます。
分割取得
件数の多いデータを複数ぺージに分割して取得します。ここでは、検索結果表示画面の1画面に必要なデータ(ここでは10件分)のみサーバーから取得します。従来のサーバー集中型でも同様の手法は行われていましたが、ぺージの表示を進めるたびにサーバーとの通信待ちが発生して不便でした。ここでは「無限スクロール」を実装しています。次に表示するぺージのデータをバックグラウンドで次々と受信することで、無限に連続したデータのようにスクロール表示します。サンプルアプリでは、下方向のみ無限スクロールの実装をしています。上下両方向の実装は関連書籍の実践編を参照して下さい。
1
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
34
35
36 | //無限スクロールの実装
//kankoClient¥src¥app¥service¥scroll.service.ts
//スクロールイベントをもとに呼びだされるメソッド
update() {
//現在のスクロール位置取得
let posY = scrollY;
this.setConst();
//下スクロール可能サイズ取得
let bottomScrollMargin = this.CONTENTS_HEIGHT - this.WINDOW_HEIGHT - posY;
//log
console.log('posY=%d,magin=%d,threshold=%d,listH=%d',
posY, bottomScrollMargin, this.THRESHOLD, bottomScrollMargin + posY + this.WINDOW_HEIGHT);
//スクロール終端で強制反転
//次ぺージのデータ供給が追いつかない場合、スクロールの終端で停止。
//スクロール位置を上に戻さないと、終端のままではスクロールできない。
//ここでは常にスクロールの余地をのこす実装をしている。
if (bottomScrollMargin < this.ROW_HEIGHT) {
console.log('スクロール強制反転');
this.move(scrollY - this.ROW_HEIGHT, 0, null);
}
//のこりスクロールサイズがTHRESHOLDの値より小さくなったら
//次データ取得
if (bottomScrollMargin < this.THRESHOLD) {
console.log('バッファ更新要求');
//次のぺージデータ取得
this.refreshBuffer();
}
}
|
Google Web developer Tools でネットワークのモニターで確認できます。
検索条件として「エリアの選択:全国」「ジャンルの選択:すべて選択」を行い1267件結果を表示してスクロールを行うと、30件づつサーバーからデータがダウンロードされてきます。
5-3. DBを使ったサーバーとのデータ交換
実装のポイント(2)
Angularでは、サーバーとのデータ連携にHttp関連のAPIを利用するのが一般的です。しかし、ここでは代わりにPouchDBのAPIを利用しています。こうすることで、通信失敗時のエラー処理やアップロード時のリトライ処理など複雑なコードを記述することなく、データのやり取りに関する処理記述で済み、コード量を大幅に削減できます。
データ処理はdata.service.tsで受け付け、db.servece.tsで実行しています。
5-3-1. 検索結果の取得
1
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
34
35
36
37
38
39
40 | //処理の受け付け
//kankoClient¥src¥app¥service¥data.service.ts
//db.serviceへ検索条件を引数としてgetDocsメソッド呼び出し
async getDocs(query: any) {
return await this.dbService.getDocs(query);
}
//処理の実行
//kankoClient¥src¥app¥service¥db.service.ts
async getDocs(query: any) {
try {
//サーバーのデータベースに対し検索条件を送信・結果を受け取る
let res = await this.remoteDb.find({
selector: {
_id: {$gt: 0},
keycode: {$in: this.stateService.state.selectedKeys}
},
use_index: "infoDb-index",//検索時に使用するindexを指定
limit: query.limit,//件数上限を指定(事前ロード、無限スクロール用)
skip: query.skip//検索結果を途中から取得(無限スクロール用)
}).catch(promiseError);
//エラー処理
if(!res){
console.log("res invarid"+res);
return [];
}
console.dir(res);
//サーバーから取得した結果の加工
return await this.postProcess(res.docs,query.skip,false).catch(promiseError);
//エラー処理
} catch (error) {
throw new Error("リモートDBからのgetDoc失敗" + error.message);
}
}
|
5-4. 観光情報検索アプリの自動生成ドキュメント
観光情報検索アプリのソースコードから、「CompoDoc」を使い自動生成したeBookを作成しました。「CompoDoc」は、プロジェクトファイルから、アプリ全体の構造や詳細な仕様などの関連ドキュメントをまとめたeBookを生成するドキュメント自動生成ツールです。
下のボタンをクリックすると自動生成したドキュメントがブラウザの新規タブに表示されます。
[参考情報]
Compodocサイト