Angularナビゲーション
このガイドでは、IonicとAngularで構築されたアプリにおけるルーティングの仕組みについて説明します。
Angular Routerは、Angularアプリケーションにおいて最も重要なライブラリの1つです。これがなければ、アプリは単一ビュー/単一コンテキストのアプリになり、ブラウザの再読み込み時にナビゲーション状態を維持できません。Angular Routerを使用すると、リンク可能でリッチなアニメーション(もちろんIonicと組み合わせた場合)を持つリッチなアプリを作成できます。Angular Routerの基本と、Ionicアプリ用に設定する方法を見てみましょう。
シンプルなルート
ほとんどのアプリでは、何らかのルートが必要になることがよくあります。最も基本的な構成は次のようになります。
import { RouterModule } from '@angular/router';
@NgModule({
imports: [
...
RouterModule.forRoot([
{ path: '', component: LoginComponent },
{ path: 'detail', component: DetailComponent },
])
],
})
ここで示している最も簡単な内訳は、パス/コンポーネントのルックアップです。アプリが読み込まれると、ルーターはユーザーが読み込もうとしているURLを読み取ることによって開始されます。このサンプルでは、ルートは `` を探します。これは基本的にインデックスルートです。そのため、これには `LoginComponent` を読み込みます。非常に簡単です。パスとコンポーネントを一致させるこのパターンは、ルーター設定にあるすべてのエントリで続きます。しかし、初期読み込みで別のパスを読み込みたい場合はどうでしょうか?
リダイレクトの処理
このためには、ルーターリダイレクトを使用できます。リダイレクトは、一般的なルートオブジェクトと同じように機能しますが、いくつかの異なるキーが含まれています。
[
{ path: '', redirectTo: 'login', pathMatch: 'full' },
{ path: 'login', component: LoginComponent },
{ path: 'detail', component: DetailComponent },
];
リダイレクトでは、アプリのインデックスパスを探します。それが読み込まれたら、`login` ルートにリダイレクトします。 `pathMatch` の最後のキーは、ルーターがパスをどのように検索するかを指示するために必要です。
`full` を使用しているので、パスが `/route1/route2/route3` のようなものになっても、フルパスを比較する必要があることをルーターに指示しています。つまり、
{ path: '/route1/route2/route3', redirectTo: 'login', pathMatch: 'full' },
{ path: 'login', component: LoginComponent },
があり、`/route1/route2/route3` を読み込むと、リダイレクトされます。ただし、`/route1/route2/route4` を読み込んだ場合は、パスが完全に一致しないため、リダイレクトされません。
代わりに、
{ path: '/route1/route2', redirectTo: 'login', pathMatch: 'prefix' },
{ path: 'login', component: LoginComponent },
を使用した場合、`/route1/route2/route3` と `/route1/route2/route4` の両方を読み込むと、両方のルートでリダイレクトされます。これは、`pathMatch: 'prefix'` がパスの 一部のみと一致するためです。
異なるルートへの移動
ルートについて話すのは良いことですが、実際にどのようにしてそのルートに移動するのでしょうか?このためには、`routerLink` ディレクティブを使用できます。前に戻って、シンプルなルーター設定を見てみましょう。
RouterModule.forRoot([
{ path: '', component: LoginComponent },
{ path: 'detail', component: DetailComponent },
]);
これで、`LoginComponent` から、次のHTMLを使用して詳細ルートに移動できます。
<ion-header>
<ion-toolbar>
<ion-title>Login</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-button [routerLink]="['/detail']">Go to detail</ion-button>
</ion-content>
ここで重要なのは、`ion-button` と `routerLink` ディレクティブです。 RouterLinkは、一般的な `href` と同じ考え方に基づいて機能しますが、URLを文字列として構築する代わりに、配列として構築できます。これにより、より複雑なパスを提供できます。
また、ルーターAPIを使用して、アプリ内でプログラムによって移動することもできます。
import { Component } from '@angular/core';
import { Router } from '@angular/router';
@Component({
...
})
export class LoginComponent {
constructor(private router: Router){}
navigate(){
this.router.navigate(['/detail'])
}
}
どちらのオプションも同じナビゲーションメカニズムを提供しますが、異なるユースケースに適合します。
LocationStrategy.historyGo を使用したナビゲーション
Angular Routerには、開発者がアプリケーションの履歴を前後に移動できる LocationStrategy.historyGo メソッドがあります。例を見てみましょう。
次のようなアプリケーション履歴があるとします
`/pageA` --> `/pageB` --> `/pageC`
`/pageC` で `LocationStrategy.historyGo(-2)` を呼び出すと、`/pageA` に戻ります。次に `LocationStrategy.historyGo(2)` を呼び出すと、`/pageC` に移動します。
`LocationStrategy.historyGo()` の重要な特徴は、アプリケーションの履歴が線形であることを想定していることです。これは、非線形ルーティングを使用するアプリケーションでは `LocationStrategy.historyGo()` を使用しないでください。詳細については、線形ルーティングと非線形ルーティング を参照してください。
遅延読み込みルート
現在のルートの設定方法では、ルート app.module と同じチャンクに含まれるようになります。これは理想的ではありません。代わりに、ルーターには、コンポーネントを独自のチャンクに分離できる設定があります。
import { RouterModule } from '@angular/router';
@NgModule({
imports: [
...
RouterModule.forRoot([
{ path: '', redirectTo: 'login', pathMatch: 'full' },
{ path: 'login', loadChildren: () => import('./login/login.module').then(m => m.LoginModule) },
{ path: 'detail', loadChildren: () => import('./detail/detail.module').then(m => m.DetailModule) }
])
],
})
似ていますが、`loadChildren` プロパティは、コンポーネントを直接ではなく、ネイティブインポートを使用してモジュールを参照する方法です。ただし、これを行うには、各コンポーネントのモジュールを作成する必要があります。
...
import { RouterModule } from '@angular/router';
import { LoginComponent } from './login.component';
@NgModule({
imports: [
...
RouterModule.forChild([
{ path: '', component: LoginComponent },
])
],
})
追加のコンテンツは除外して、必要な部分のみを含めています。
ここでは、一般的なAngularモジュール設定とRouterModuleインポートがありますが、現在は `forChild` を使用してその設定でコンポーネントを宣言しています。この設定では、ビルドを実行すると、アプリコンポーネント、ログインコンポーネント、詳細コンポーネントのそれぞれに個別のチャンクが生成されます。
スタンドアロンコンポーネント
スタンドアロンコンポーネントを使用すると、開発者はコンポーネントをAngularモジュールに宣言することなく、ルート上のコンポーネントを遅延読み込みできます。
開発者は、Angularのスタンドアロンコンポーネントルーティングの既存の構文を使用できます
@NgModule({
imports: [
RouterModule.forRoot([
{
path: 'standalone-route',
loadComponent: () => import('./path/to/my-component.component').then((c) => c.MyComponent),
},
]),
],
})
export class AppRoutingModule {}
`routerLink`、`routerDirection`、または `routerAction` を使用している場合は、Ionicコンポーネントの場合は `IonRouterLink` ディレクティブ、`<a>` 要素の場合は `IonRouterLinkWithHref` ディレクティブもインポートしてください。この例は、Ionic Angularビルドオプションのドキュメントにあります。
スタンドアロンコンポーネントの使用を開始するには、Angularの公式ドキュメントをご覧ください。
ライブサンプル
上記の概念とコードを実際に体験したい場合は、StackBlitzにある上記のトピックのライブサンプルをご覧ください。
リニアルーティングと非リニアルーティング
リニアルーティング
ルーティングを使用するウェブアプリを構築したことがある場合、おそらくリニアルーティングを使用したことがあるでしょう。リニアルーティングとは、ページをプッシュおよびポップすることで、アプリケーションの履歴を前方または後方に移動できることを意味します。
以下は、モバイルアプリにおけるリニアルーティングの例です。
この例のアプリケーション履歴には、次のパスがあります。
アクセシビリティ
--> VoiceOver
--> 音声
戻るボタンを押すと、逆方向に同じルーティングパスをたどります。リニアルーティングは、シンプルで予測可能なルーティング動作を可能にするため、便利です。また、LocationStrategy.historyGo()などのAngular Router APIを使用できることも意味します。
リニアルーティングの欠点は、タブビューなどの複雑なユーザーエクスペリエンスに対応できないことです。そこで、非リニアルーティングが登場します。
非リニアルーティング
非リニアルーティングは、Ionicでモバイルアプリを構築することを学ぶ多くのウェブ開発者にとって新しい概念かもしれません。
非リニアルーティングとは、ユーザーが戻るべきビューが、必ずしも画面に表示されていた直前のビューではないことを意味します。
以下は、非リニアルーティングの例です。
上記の例では、オリジナル
タブから開始します。カードをタップすると、オリジナル
タブ内のテッド・ラッソ
ビューに移動します。
ここから、検索
タブに切り替えます。次に、オリジナル
タブをもう一度タップすると、テッド・ラッソ
ビューに戻ります。この時点で、非リニアルーティングの使用を開始しています。
なぜこれが非リニアルーティングなのでしょうか?直前に表示されていたビューは検索
ビューでした。しかし、テッド・ラッソ
ビューの戻るボタンを押すと、ルートのオリジナル
ビューに戻るはずです。これは、モバイルアプリの各タブが独自のスタックとして扱われるためです。タブの操作セクションで、これについて詳しく説明します。
テッド・ラッソ
ビューから戻るボタンをタップするだけでLocationStrategy.historyGo(-1)
が呼び出された場合、正しくない検索
ビューに戻ってしまいます。
非リニアルーティングは、リニアルーティングでは処理できない高度なユーザーフローを可能にします。ただし、LocationStrategy.historyGo()
など、特定のリニアルーティングAPIはこの非リニア環境では使用できません。つまり、タブまたはネストされたアウトレットを使用する場合は、LocationStrategy.historyGo()
を使用しないでください。
どちらを選択すべきですか?
非リニアルーティングが必要になるまで、アプリケーションは可能な限りシンプルにしておくことをお勧めします。非リニアルーティングは非常に強力ですが、モバイルアプリケーションにかなりの複雑さを加えます。
非リニアルーティングの2つの最も一般的な用途は、タブとネストされたion-router-outlet
です。アプリケーションがタブまたはネストされたルーターアウトレットのユースケースを満たしている場合にのみ、非リニアルーティングを使用することをお勧めします。
タブの詳細については、タブの操作を参照してください。
ネストされたルーターアウトレットの詳細については、ネストされたルートを参照してください。
共有URLとネストされたルート
ルーティングを設定する際のよくある混乱点は、共有URLとネストされたルートのどちらを選択するかです。このガイドでは、両方について説明し、どちらを使用するかを決定するのに役立ちます。
共有URL
共有URLは、ルートがURLの一部を共通して持つルート構成です。以下は、共有URL構成の例です。
const routes: Routes = [
{
path: 'dashboard',
component: DashboardMainPage,
},
{
path: 'dashboard/stats',
component: DashboardStatsPage,
},
];
上記のルートは、URLのdashboard
部分を再利用するため、「共有」と見なされます。
ネストされたルート
ネストされたルートは、ルートが他のルートの子としてリストされるルート構成です。以下は、ネストされたルート構成の例です。
const routes: Routes = [
{
path: 'dashboard',
component: DashboardRouterOutlet,
children: [
{
path: '',
component: DashboardMainPage,
},
{
path: 'stats',
component: DashboardStatsPage,
},
],
},
];
上記のルートは、親ルートのchildren
配列にあるため、ネストされています。親ルートはDashboardRouterOutlet
コンポーネントをレンダリングすることに注意してください。ルートをネストする場合は、ion-router-outlet
の別のインスタンスをレンダリングする必要があります。
どちらを選択すべきですか?
共有URLは、URL内の2つのページ間の関係を保持しながら、ページAからページBに遷移する場合に最適です。前の例では、/dashboard
ページのボタンは/dashboard/stats
ページに遷移できます。2つのページ間の関係は、a)ページ遷移とb)URLのために保持されます。
ネストされたルートは、アウトレットAにコンテンツをレンダリングし、ネストされたアウトレットB内にサブコンテンツをレンダリングする場合に使用する必要があります。最も一般的なユースケースはタブです。タブIonicスターターアプリケーションをロードすると、最初のion-router-outlet
にion-tab-bar
とion-tabs
コンポーネントがレンダリングされます。ion-tabs
コンポーネントは、各タブのコンテンツのレンダリングを担当する別のion-router-outlet
をレンダリングします。
モバイルアプリケーションでネストされたルートが理にかなうユースケースはごくわずかです。疑問がある場合は、共有URLルート構成を使用してください。タブ以外でネストされたルーティングを使用すると、アプリのナビゲーションがすぐに混乱する可能性があるため、強くお勧めしません。
タブの操作
タブを使用すると、Angular RouterはIonicにどのコンポーネントをロードする必要があるかを知るメカニズムを提供しますが、実際にはタブコンポーネントによって重い処理が行われます。簡単な例を見てみましょう。
const routes: Routes = [
{
path: 'tabs',
component: TabsPage,
children: [
{
path: 'tab1',
children: [
{
path: '',
loadChildren: () => import('../tab1/tab1.module').then((m) => m.Tab1PageModule),
},
],
},
{
path: '',
redirectTo: '/tabs/tab1',
pathMatch: 'full',
},
],
},
{
path: '',
redirectTo: '/tabs/tab1',
pathMatch: 'full',
},
];
ここでは、ロードする「tabs」パスがあります。この例ではパスを「tabs」と呼びますが、パスの名前は変更できます。アプリに合った名前を付けることができます。そのルートオブジェクトでは、子ルートも定義できます。この例では、トップレベルの子ルート「tab1」が「アウトレット」として機能し、追加の子ルートをロードできます。この例では、単一のサブ子ルートがあり、新しいコンポーネントをロードするだけです。タブのマークアップは次のとおりです。
<ion-tabs>
<ion-tab-bar slot="bottom">
<ion-tab-button tab="tab1">
<ion-icon name="flash"></ion-icon>
<ion-label>Tab One</ion-label>
</ion-tab-button>
</ion-tab-bar>
</ion-tabs>
以前にIonicでアプリを構築したことがある場合、これはなじみ深いはずです。 ion-tabs
コンポーネントを作成し、ion-tab-bar
を提供します。 ion-tab-bar
は、ルーター設定のタブ「アウトレット」に関連付けられたtab
プロパティを持つion-tab-button
を提供します。 @ionic/angular
の最新バージョンでは、<ion-tab>
は不要になり、開発者はタブバーを完全にカスタマイズできるようになり、唯一の真実のソースはルーター設定内に存在することに注意してください。
Ionicのタブの仕組み
Ionicの各タブは、個別のナビゲーションスタックとして扱われます。これは、アプリケーションに3つのタブがある場合、各タブに独自のナビゲーションスタックがあることを意味します。各スタック内では、前方に移動(ビューのプッシュ)したり、後方に移動(ビューのポップ)したりできます。
この動作は、他のWebベースのUIライブラリにあるほとんどのタブ実装とは異なるため、注意することが重要です。他のライブラリは通常、タブを1つの履歴スタックとして管理します。
Ionicは開発者がモバイルアプリを構築するのを支援することに重点を置いているため、Ionicのタブはネイティブモバイルタブのできるだけ近くなるように設計されています。その結果、Ionicのタブには、他のUIライブラリで見たタブ実装とは異なる動作がある場合があります。これらの違いのいくつかについて詳しくは、以下をお読みください。
タブ内の子ルート
タブに追加のルートを追加する場合は、親タブをパスプレフィックスとして兄弟ルートとして記述する必要があります。以下の例では、/tabs/tab1/view
ルートを/tabs/tab1
ルートの兄弟として定義しています。この新しいルートにはtab1
プレフィックスがあるため、Tabs
コンポーネント内にレンダリングされ、タブ1はion-tab-bar
で選択されたままになります。
const routes: Routes = [
{
path: 'tabs',
component: TabsPage,
children: [
{
path: 'tab1',
children: [
{
path: '',
loadChildren: () => import('../tab1/tab1.module').then((m) => m.Tab1PageModule),
},
],
},
{
path: 'tab1/view',
children: [
{
path: '',
loadChildren: () => import('../tab1/tab1view.module').then((m) => m.Tab1ViewPageModule),
},
],
},
{
path: 'tab2',
children: [
{
path: '',
loadChildren: () => import('../tab2/tab2.module').then((m) => m.Tab2PageModule),
},
],
},
{
path: 'tab3',
children: [
{
path: '',
loadChildren: () => import('../tab3/tab3.module').then((m) => m.Tab3PageModule),
},
],
},
],
},
{
path: '',
redirectTo: '/tabs/tab1',
pathMatch: 'full',
},
];
タブ間の切り替え
各タブは独自のナビゲーションスタックであるため、これらのナビゲーションスタックが相互作用することはありません。これは、タブ1にユーザーをタブ2にルーティングするボタンがあってはならないことを意味します。言い換えれば、タブはユーザーがタブバーのタブボタンをタップすることによってのみ変更されるべきです。
この実際の良い例は、iOS App StoreとGoogle Playストアのモバイルアプリケーションです。これらのアプリはどちらもタブ付きインターフェースを提供しますが、どちらもユーザーをタブ間でルーティングしません。たとえば、iOS App Storeアプリの「ゲーム」タブはユーザーを「検索」タブに誘導することはなく、その逆もありません。
タブで発生する一般的な間違いをいくつか見てみましょう。
複数のタブが参照する設定タブ
一般的な方法は、設定ビューを独自のタブとして作成することです。これは、開発者がいくつかのネストされた設定メニューを表示する必要がある場合に最適です。ただし、他のタブは設定タブにルーティングしようとしてはなりません。上記で述べたように、設定タブをアクティブにする唯一の方法は、ユーザーが適切なタブボタンをタップすることです。
タブが設定タブを参照する必要がある場合は、ion-modal
を使用して設定ビューをモーダルにすることをお勧めします。これは、iOS App Storeアプリで見られる方法です。このアプローチでは、どのタブでもモバイルタブのパターン(各タブが独自のスタックである)を壊すことなく、モーダルを表示できます。
以下の例は、iOS App Storeアプリが複数のタブから「アカウント」ビューを表示する方法を示しています。アプリは「アカウント」ビューをモーダルで表示することで、モバイルタブのベストプラクティス内で動作し、複数のタブで同じビューを表示できます。
タブ間でビューを再利用する
もう1つの一般的な方法は、複数のタブに同じビューを表示することです。開発者はしばしば、ビューを単一のタブに含め、他のタブはそのタブにルーティングすることでこれを行おうとします。上記で述べたように、これはモバイルタブのパターンを壊すため、避けるべきです。
代わりに、各タブに同じコンポーネントを参照するルートを用意することをお勧めします。これは、Spotifyのような人気のあるアプリで行われている方法です。たとえば、「ホーム」、「検索」、「ライブラリ」タブからアルバムまたはポッドキャストにアクセスできます。アルバムまたはポッドキャストにアクセスすると、ユーザーはそのタブ内に留まります。アプリは、タブごとにルートを作成し、コードベースで共通のコンポーネントを共有することでこれを実現します。
以下の例は、Spotifyアプリが同じアルバムコンポーネントを再利用して複数のタブにコンテンツを表示する方法を示しています。各スクリーンショットは同じアルバムを示していますが、タブが異なっていることに注意してください。
ホームタブ | 検索タブ |
---|---|
![]() | ![]() |