Ionic vs. Angular

Ionic 與 Angular 比較

建立 Ionic 專案中,我們練習了如何建立 Ionic,接下來對於 會開發 Angular 程式的人來說,最關注的應該就是 Ionic 跟 Angular 有什麼不同?要再多學些什麼?

啟動流程

我們依照程式執行順序來比較一下差異:

index.html

從 Browser 或 WebView 第一個取得的就是 index.html 網頁,在裡面我們可以看到 Ionic 多載入了 cordova.js,就像之前說過的,我們可以看成 Ionic = Angular + Cordova,本篇我們比較的是 Ionic 的 Angular 部分跟標準 Angular 程式有什麼差異,不會涵蓋 Cordova。
另一個亮點就是載入了 manifest.json 以及一段被註解起來負責執行 service worker 的 JavaScript,專案目錄下也看到一個 service-worker.js 檔案,由此可見 Ionic 本身也考慮到 PWA (Progressive Web App) 的模式,Angular 目前要自己手動加入,但是未來的版本應該可以期待。

index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// [Angular] index.html
<!doctype html>
<html lang="en">

<head>
<meta charset="utf-8">
<title>FirstApp</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></app-root>
</body>

</html>

index.html
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
// [Ionic] index.html
<!doctype html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<title>Ionic App</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="format-detection" content="telephone=no">
<meta name="msapplication-tap-highlight" content="no">

<link rel="icon" type="image/x-icon" href="assets/icon/favicon.ico">
<link rel="manifest" href="manifest.json">
<meta name="theme-color" content="#4e8ef7">

<!-- cordova.js required for cordova apps -->
<script src="cordova.js"></script>

<!-- un-comment this code to enable service worker
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('service-worker.js')
.then(() => console.log('service worker installed'))
.catch(err => console.error('Error', err));
}
</script>-->

<link href="build/main.css" rel="stylesheet">

</head>
<body>

<!-- Ionic's root component and where the app will load -->
<ion-app></ion-app>

<!-- The polyfills js is generated during the build process -->
<script src="build/polyfills.js"></script>

<!-- The vendor js is generated during the build process
It contains all of the dependencies in node_modules -->
<script src="build/vendor.js"></script>

<!-- The main bundle js is generated during the build process -->
<script src="build/main.js"></script>

</body>
</html>

對 Angular 來說我們是透過瀏覽器對某個網址所對應的 Server 發出 get 來取得網頁。
對 Ionic App 來說它是靠 Cordova 幫我們去將某個網頁載入到 WebView 控制項內,所以我們透過設定檔-config.xml 來預先告知 Cordova 程式開啟時要先載入哪個網頁。
img

main.ts

在 Angular 專案中,我們可以透過設定檔-.angular-cli.json 看到會優先執行的 js 檔。
img
而在 Ionic 則是直接宣告在網頁 body最後一行,因 Ionic 所有檔案都已經在本地端(手機內)了,所以不需要考慮延遲載入的需求,也因此它會先將其他組件優先載入,確保執行時所有組件都找的到。
img
比較一下 main.ts 可以說是一樣的,因為他們都指定了 AppModule 為起始模組。

當頁面資料過大時,其實有可能也需要延遲載入的機制來做緩衝,Ionic 3 也幫我們顧慮到了,它的實作方式非常簡單,簡單到你已經套用了延遲載入卻沒有發覺。

main.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// [Angular] main.ts
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import 'hammerjs';

if (environment.production) {
enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule);

main.ts
1
2
3
4
5
6
7
// [ionic] main.ts
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app.module';

platformBrowserDynamic().bootstrapModule(AppModule);

app.module.ts

接著比較 app.module.ts 可以看到 Angular 在 起始模組(AppModule) 中透過 bootstrap 屬性指定了 AppComponent起始元件

app.module.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// [Angular] src\app\app.module.ts 
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
AppRoutingModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

而 Ionic 看起來似乎也透過 bootstrap 屬性指定了 IonicApp,不過仔細看一下 IonicApp 是來自 ionic-angular,查看一下它的原始碼,似乎 IonicApp 也不算是一個 Angular Component,而在 imports 屬性上發現 MyApp 這個 Component 不是直接被加入,而是多個 IonicModule.forRoot(MyApp) 將它註記成起始元件,我們可以 Ionic 的模組就此時開始介入。

開啟 src\app\app.component.ts 可以看到類別名稱就是 MyApp,在 Angular 專案內預設會是叫做 AppComponent
img
因為目前並沒有深入研究 IonicModule.forRoot 背後運作機制,所以不確定將它解釋成註記是否正確,目前只能確定 Ionic 起始內容是此方法所指定的 Component。

app.module.ts
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
// [Ionic] src\app\app.module.ts 
import { NgModule, ErrorHandler } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular';
import { MyApp } from './app.component';

import { AboutPage } from '../pages/about/about';
import { ContactPage } from '../pages/contact/contact';
import { HomePage } from '../pages/home/home';
import { TabsPage } from '../pages/tabs/tabs';

import { StatusBar } from '@ionic-native/status-bar';
import { SplashScreen } from '@ionic-native/splash-screen';

@NgModule({
declarations: [
MyApp,
AboutPage,
ContactPage,
HomePage,
TabsPage
],
imports: [
BrowserModule,
IonicModule.forRoot(MyApp)
],
bootstrap: [IonicApp],
entryComponents: [
MyApp,
AboutPage,
ContactPage,
HomePage,
TabsPage
],
providers: [
StatusBar,
SplashScreen,
{provide: ErrorHandler, useClass: IonicErrorHandler}
]
})
export class AppModule {}

img

AppComponent vs. MyApp

在 Angular 中透過 @Component 這個裝飾器為 AppComponent 加入了選擇器屬性-selector,Angular 會自動將網頁內含有該選擇器的 tag 替換成 AppComponent 的樣板。

app.component.ts
1
2
3
4
5
6
7
8
9
10
11
// [Angular] src\app\app.component.ts
import { Component } from '@angular/core';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
}

切換到 Ionic 可以發現 MyApp 的 @Component 裝飾器竟然沒有設定 selector 屬性,由剛才的 AppModule 說道 IonicModule.forRoot(MyApp) 的這個方法,可以知道 Ionic 是透過直接明確指定類別的方式宣告起始元件

app.component.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// [Ionic] src\app\app.component.ts
import { Component } from '@angular/core';
import { Platform } from 'ionic-angular';
import { StatusBar } from '@ionic-native/status-bar';
import { SplashScreen } from '@ionic-native/splash-screen';

import { TabsPage } from '../pages/tabs/tabs';

@Component({
templateUrl: 'app.html'
})
export class MyApp {
rootPage:any = TabsPage;

constructor(platform: Platform, statusBar: StatusBar, splashScreen: SplashScreen) {
platform.ready().then(() => {
// Okay, so the platform is ready and our plugins are available.
// Here you can do any higher level native things you might need.
statusBar.styleDefault();
splashScreen.hide();
});
}
}

眼尖的人其實可以發現 Ionic 預設在 AppModule 內比 Angular 專案多設定了 entryComponents 屬性。
查詢 Angular 官網的 Dynamic Component Loader 可以看到說明,但是說明感覺很籠統,目前看起來應該是說只要是透過 ViewContainerRef.createComponent() 來建立的元件都需要註冊到 entryComponents
img

從建構式內還可看到 splashScreen.hide();,Ionic 也提供給我們可以設定程式啟動的等待畫面。
statusBar.styleDefault(); 則是讓我們設定手機最上方的狀態列,例如我們希望 App 佔滿整個螢幕時可以把狀態列隱藏起來。

Angular 路由 vs. Ionic 導覽

開啟 Angular 專案 src\app\app.component.html,我們可以看到最後一行也是最重要的一行 <router-outlet></router-outlet>,Angular 的 路由模組(RouterModule) 就是透過這個 路由插座(router-outlet) 來插入符合路由規則的元件。

app.component.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// [Angular] src\app\app.component.html
<!--The content below is only a placeholder and can be replaced.-->
<div style="text-align:center">
<h1>
Welcome to Ionic vs. Angular!
</h1>
<img width="300" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg==">
</div>
<h2>Here are some links to help you start: </h2>
<ul>
<li>
<h2><a target="_blank" href="https://angular.io/tutorial">Tour of Heroes</a></h2>
</li>
<li>
<h2><a target="_blank" href="https://github.com/angular/angular-cli/wiki">CLI Documentation</a></h2>
</li>
<li>
<h2><a target="_blank" href="https://blog.angular.io/">Angular blog</a></h2>
</li>
</ul>

<router-outlet></router-outlet>

再來看 Ionic 專案可以發現預設並沒有加入 路由模組,更沒有類似 app-routing.module.ts 可以設定路由規則的檔案,開啟 src\app\app.html 可以看到 Ionic 改用自己的導覽元件-ion-nav,並將 root 屬性指定給 rootPage 變數。

app.html
1
2
// [Ionic] src\app\app.html
<ion-nav [root]="rootPage"></ion-nav>

再回過頭看看 app.component.tsrootPage 變數被設定成 TabsPage Component。
img

從上面我們可以概略知道以往我們在 Angular 上很習慣的會利用路由模組來作頁面切換(Angular 屬於 SPA 程式,這邊的頁面不是只整個 HTML 版面,而是指主要操作區域),而在 Ionic 上 卻是透過 ion-nav 來處理。
ion-nav 是什麼?

ion-nav is the declarative component for a NavController.

在查詢官方文件可以知道 NavController 是導航控制器組件的基底類別,目前 NavTab 2個 Component 都繼承自它-NavControllerBase
img
img
查詢 NavControllerBase 程式結構,我們可以看到一些導覽方法,例如:pushpopinsertremove

nav-controller-base.d.ts
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
// [Ionic] node_modules\ionic-angular\navigation\nav-controller-base.d.ts
import { ComponentFactoryResolver, ComponentRef, ElementRef, ErrorHandler, EventEmitter, NgZone, Renderer, ViewContainerRef } from '@angular/core';
...
/**
* @hidden
* This class is for internal use only. It is not exported publicly.
*/
export declare class NavControllerBase extends Ion implements NavController {
parent: any;
_app: App;
config: Config;
...
constructor(parent: any, _app: App, config: Config, plt: Platform, elementRef: ElementRef, _zone: NgZone, renderer: Renderer, _cfr: ComponentFactoryResolver, _gestureCtrl: GestureController, _trnsCtrl: TransitionController, _linker: DeepLinker, _domCtrl: DomController, _errHandler: ErrorHandler);
push(page: any, params?: any, opts?: NavOptions, done?: TransitionDoneFn): Promise<any>;
insert(insertIndex: number, page: any, params?: any, opts?: NavOptions, done?: TransitionDoneFn): Promise<any>;
insertPages(insertIndex: number, insertPages: any[], opts?: NavOptions, done?: TransitionDoneFn): Promise<any>;
pop(opts?: NavOptions, done?: TransitionDoneFn): Promise<any>;
popTo(indexOrViewCtrl: any, opts?: NavOptions, done?: TransitionDoneFn): Promise<any>;
popToRoot(opts?: NavOptions, done?: TransitionDoneFn): Promise<any>;
popAll(): Promise<any[]>;
remove(startIndex: number, removeCount?: number, opts?: NavOptions, done?: TransitionDoneFn): Promise<any>;
removeView(viewController: ViewController, opts?: NavOptions, done?: TransitionDoneFn): Promise<any>;
setRoot(pageOrViewCtrl: any, params?: any, opts?: NavOptions, done?: TransitionDoneFn): Promise<any>;
setPages(viewControllers: any[], opts?: NavOptions, done?: TransitionDoneFn): Promise<any>;
_queueTrns(ti: TransitionInstruction, done: TransitionDoneFn): Promise<boolean>;
_success(result: NavResult, ti: TransitionInstruction): void;
_failed(rejectReason: any, ti: TransitionInstruction): void;
_fireError(rejectReason: any, ti: TransitionInstruction): void;
_nextTrns(): boolean;
_startTI(ti: TransitionInstruction): Promise<void>;
_loadLazyLoading(ti: TransitionInstruction): Promise<void>;
_getEnteringView(ti: TransitionInstruction, leavingView: ViewController): ViewController;
_postViewInit(enteringView: ViewController, leavingView: ViewController, ti: TransitionInstruction): void;
/**
* DOM WRITE
*/
...
}

其實這樣看起來 Nav(ion-nav) 與 Tab(ion-tab) 與 Angular 的 路由插座(router-outlet) 相比更加強大,因為它們本身就提供的頁面切換的方法,可以將它本身的 content 內容替換成我們指定的元件類別,當然這都是因為它們繼承了 NavControllerBase。

Page

與 Angular 一樣我們可以透過下列指令協助我們快速建立各種類型檔案:
ionic generate [<type>] [<name>]

generate 可以縮寫成 g

Ionic CLI 所提供的類型有:component, directive, page, pipe, provider, tabs,其中 pagetabs 是 Angular 所沒有的,所以我們就先建立一個 page 試試,指令如下:
ionic g page page1

這邊筆者覺得 Angular CLI 做得比較好,因為 Angular CLI 會標示出建立那些檔案、修改那些檔案,Ionic CLI 目前只有一行成功訊息,這對於剛接觸的人可能會比較不親合。
img

我們可以看到 Ionic CLI 會依類型建立資料夾,並將新建的類型檔案放置到對應的資料夾下。Angular CLI 則是統一放置在 src\app\ 下面。
img

接著我們開啟 src\pages\page1\page1.ts,我們可以看到熟悉的裝飾器-@Component,也就是說 page 本質上還是 Angular Component。

page1.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// [Ionic] src\pages\page1\page1.ts
import { Component } from '@angular/core';
import { IonicPage, NavController, NavParams } from 'ionic-angular';

@IonicPage()
@Component({
selector: 'page-page1',
templateUrl: 'page1.html',
})
export class Page1Page {

constructor(public navCtrl: NavController, public navParams: NavParams) {
}

ionViewDidLoad() {
console.log('ionViewDidLoad Page1Page');
}

}

比較一下可以發現 Ionic Page 多了一些東西:

  • NavController 參數:我們已經知道 Ionic 是透過導覽模式來切換頁面,所以 Ionic CLI 貼心透過依賴注入(Dependency Injection,簡稱DI)的幫我們將 NavController 加到頁面類別內。

    我們可以看到 constructor 內的參數都多了存取修飾詞-public,此為 TypeScript 的語法糖,我們可以想成它會自動在 class 內建立一個同名的全域變數,並將值指向該變數,我們在其他方法內就可以直接透過 this.navCtrl.xxx 來使用 NavController

  • ionViewDidLoad():就像我們透過 Angular CLI 來建立 Component 時它會幫我們增加 ngOnInit() 方法一樣,這些方法都牽涉到生命週期。
    img
    查詢官方網站可以看到 NavController Lifecycle events。
    img

  • @IonicPage() 裝飾器:開啟其他預先建立好的 page 可以發現既沒有 Module 檔案,class 內也沒加上 @IonicPage() 裝飾器。
    img
    反過來看,這些預設的 page 卻跟 Angular Component 一樣被加入到 AppModuledeclarations,也同時設定到 entryComponents 屬性。

    Angular 的 Component 必須註冊到 NgModule 內才可以使用。

    img

  • page1.module.ts:開啟 src\pages\page1\page1.module.ts 可以看到這是一個 Angular Module,
    Page1Page 被註冊到這個 NgModule 內。

    page1.module.ts
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // [Ionic] src\pages\page1\page1.module.ts
    import { NgModule } from '@angular/core';
    import { IonicPageModule } from 'ionic-angular';
    import { Page1Page } from './page1';

    @NgModule({
    declarations: [
    Page1Page,
    ],
    imports: [
    IonicPageModule.forChild(Page1Page)
    ],
    })
    export class Page1PageModule {}

延遲載入 (Lazy loaded)

接下來我們在 Home 的樣板上面加入一個按鈕,並在其點擊事件內透過 NavController 導覽至 Page1,開啟並編輯 src\pages\home\home.html

home.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// [Ionic] src\pages\home\home.html
<ion-header>
<ion-navbar>
<ion-title>Home</ion-title>
</ion-navbar>
</ion-header>

<ion-content padding>
...
<p>
Take a look at the <code>src/pages/</code> directory to add or change tabs,
update any existing page or create new pages.
</p>
<button ion-button (click)="navPage1()">Page 1</button>
</ion-content>

接著編輯 src\pages\home\home.ts,要注意的是傳給 navCtrl.push 的參數是 'Page1Page' 字串而不是 Page1Page 類別。

home.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// [Ionic] src\pages\home\home.ts
import { Component } from '@angular/core';
import { NavController } from 'ionic-angular';

@Component({
selector: 'page-home',
templateUrl: 'home.html'
})
export class HomePage {

constructor(public navCtrl: NavController) {

}

navPage1() {
this.navCtrl.push('Page1Page');
}
}

透過 ionic serve 來啟動專案,從瀏覽器上看頁面導覽效果已經出現了。
img
img
做到這般看似正常,但是有點怪怪的,Page1Page 這個 Component 雖然註冊到了 Page1PageModule 這個 NgModule,但是 Page1PageModule 並沒有註冊到起始模組-AppModule 內,所以依照 Angular 的邏輯來推論,AppModule 並不認得 Page1Page,所以在載入時應該會出現錯誤,怎麼 Ionic 上卻沒有問題?
原來這就是 Ionic 的延遲載入機制,也就是說透過 Ionic CLI 來建立 page 時,它就會以延遲載入的架構來建立相關檔案,延遲載入的重點架構如下:

  • Component 類別必須加上 @IonicPage() 裝飾器。

  • 在 Component 路徑必須要有一個同名的 <component name>.module.ts 的 NgModule,只要名稱或路徑位置不對,CLI 編譯就會出錯,例如我們將 NgModule 改名為 page2.module.ts
    img

  • NgModule 內除了註冊 Component 外,還需再 imports 屬性內加入 IonicPageModule.forChild(<component>)

  • NavController 透過 字串(string) 來指定要導覽到該頁面,如果將字串改成 Component 的類別執行導覽時就會出現錯誤。
    img

反過來說如果我們不要使用延遲載入的話要如何調整,如同剛剛修改的 home.ts

home.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// [Ionic] src\pages\home\home.ts
import { Component } from '@angular/core';
import { NavController } from 'ionic-angular';
import { Page1Page } from '../page1/page1';

@Component({
selector: 'page-home',
templateUrl: 'home.html'
})
export class HomePage {

constructor(public navCtrl: NavController) {

}

navPage1() {
this.navCtrl.push(Page1Page);
}
}

  • 當 Component 已經有對應的 ngModule (page1.module.ts) 時,我們可以直接將該 Module 註冊到啟動模組內,或是啟動模組的延伸模組,將 src\app\app.module.ts 修改如下,再重新執行專案,應該就會正常。
app.module.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// [Ionic] src\app\app.module.ts 
import { NgModule, ErrorHandler } from '@angular/core';
...
import { Page1PageModule } from '../pages/page1/page1.module';

@NgModule({
declarations: [
...
],
imports: [
BrowserModule,
IonicModule.forRoot(MyApp),
Page1PageModule
],
bootstrap: [IonicApp],
entryComponents: [
...
],
providers: [
...
]
})
export class AppModule {}

  • 當 Component 沒有對應的 ngModule 時,那就要跟預設的 page 一樣,將 Component 註冊到啟動模組的 declarations 以及 entryComponents 屬性內,將 src\app\app.module.ts 修改如下,再重新執行專案,應該同樣可以正常運作。
app.module.ts
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
// [Ionic] src\app\app.module.ts 
import { NgModule, ErrorHandler } from '@angular/core';
...
import { Page1Page } from '../pages/page1/page1';

@NgModule({
declarations: [
...
TabsPage,
Page1Page
],
imports: [
BrowserModule,
IonicModule.forRoot(MyApp)
],
bootstrap: [IonicApp],
entryComponents: [
...
TabsPage,
Page1Page
],
providers: [
...
]
})
export class AppModule {}

其實如果仔細觀察,沒有延遲載入時頁面切換會有滑動效果,反之則沒有,在設計上這一點可能也要考量一下,例如操作功能的切換頁面可能透過延遲載入比較適合,因為我們大部分會透過 tabs 或是 menu 選單來做切換,但是同一個功能內的頁面切換不要透過延遲載入比較適合,因為上下頁面切換的滑動效果會讓整個功能比較有整體感。
img

參考文件:Ionic and Lazy Loading Pt 1

後進先出 (LIFO:Last In First Out)

我們先修改 page1 樣板,加入 input 輸入方塊,一個導覽按鈕,點擊時會再次導覽回 page1,一個導覽返回鈕,接著在類別內加入一個 page_length 屬性並給予 NavController 目前頁面堆疊數量,最後再透過嵌入繫結方式繫結到樣板上。

Ionic 的 Component 都已經預先載入了,所以可以直接使用。

page1.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// [Ionic] src\pages\page1\page1.html 
<ion-header>

<ion-navbar>
<ion-title>page1</ion-title>
</ion-navbar>

</ion-header>


<ion-content padding>
Page Length:
<hr>
<ion-item>
<ion-label fixed>Value:</ion-label>
<ion-input type="text" value=""></ion-input>
</ion-item>
<button ion-button (click)="navCtrl.push('Page1Page')">Page 1</button>
<button ion-button (click)="navCtrl.pop()">Back</button>
</ion-content>

page1.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// [Ionic] src\pages\page1\page1.ts 
import { Component } from '@angular/core';
import { IonicPage, NavController, NavParams } from 'ionic-angular';

@IonicPage()
@Component({
selector: 'page-page1',
templateUrl: 'page1.html',
})
export class Page1Page {
page_length = 0;
constructor(public navCtrl: NavController, public navParams: NavParams) {
}

ionViewDidLoad() {
console.log('ionViewDidLoad Page1Page');
this.page_length = this.navCtrl.length();
}

}

啟動專案並透過瀏覽器操作。
img
由上面的操作,我們可以看到我們每次呼叫 push 方法時,Ionic 會在產生一個全新的 page1 並且取代目前頁面,從 page_length 屬性也可以印證 NavController 紀錄的頁面確實增加了,當呼叫 pop 方法時,可以發現 Ionic 會移除目前頁面並顯示上一頁面內容,我們也可以看到之前頁面的內容都會被保留下來。
img
我們可以假想 NavController 就像一個大箱子,每次 push 時就會放入一本新書,每次 pop 時就會取出最上面的一本書,我們能看得的書永遠是最後一本,因為它會被放在最上層,下層的書都會被遮住,網頁上看到的 page 也是一樣是最後載入的 page,所以我們可以說這是一種後進先出(Last In First Out)的堆疊模式,最後進去的會最先出來。
img
了解這種模式之後,可以發現它會衍生出一些問題:

  • 占用記憶體:每個頁面都需要用到記憶體來保存狀態,因此如果堆疊越多層也代表記憶體占用越多。

  • 重複的頁面:我們在某個頁面更新資料後,在後續導覽過程不小心導覽到比較舊的歷史頁面,對使用者來說會以為資料沒更新成功。

因此在設計上可以參考一些網站導航的設計原則,例如階層不要太多可能3~5層,不同功能盡量避免可以直接切換,讓導覽途徑是樹枝展狀的階層圖,而不是交錯複雜的網狀圖。

[**ionic-first-app_2017-09-19.zip**](/uploads/ionic-first-app_2017-09-19.zip)