title: Ionic vs. Angular
date: 2017-09-17 09:00
categories: Training
keywords:
在建立 Ionic 專案中,我們練習了如何建立 Ionic,接下來對於 會開發 Angular 程式的人來說,最關注的應該就是 Ionic 跟 Angular 有什麼不同?要再多學些什麼?
我們依照程式執行順序來比較一下差異:
從 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 目前要自己手動加入,但是未來的版本應該可以期待。
{% codeblock index.html lang:html %}
// [Angular] index.html
{% endcodeblock %}
{% codeblock index.html lang:html %}
// [Ionic] index.html
{% endcodeblock %}
對 Angular 來說我們是透過瀏覽器對某個網址所對應的 Server 發出 get 來取得網頁。
對 Ionic App 來說它是靠 Cordova 幫我們去將某個網頁載入到 WebView 控制項內,所以我們透過設定檔-config.xml
來預先告知 Cordova 程式開啟時要先載入哪個網頁。
在 Angular 專案中,我們可以透過設定檔-.angular-cli.json
看到會優先執行的 js 檔。
而在 Ionic 則是直接宣告在網頁 body
的最後一行,因 Ionic 所有檔案都已經在本地端(手機內)了,所以不需要考慮延遲載入的需求,也因此它會先將其他組件優先載入,確保執行時所有組件都找的到。
比較一下 main.ts
可以說是一樣的,因為他們都指定了 AppModule 為起始模組。
當頁面資料過大時,其實有可能也需要延遲載入的機制來做緩衝,Ionic 3 也幫我們顧慮到了,它的實作方式非常簡單,簡單到你已經套用了延遲載入卻沒有發覺。
{% codeblock main.ts lang:ts %}
// [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);
{% endcodeblock %}
{% codeblock main.ts lang:ts %}
// [ionic] main.ts
import { platformBrowserDynamic } from ‘@angular/platform-browser-dynamic’;
import { AppModule } from ‘./app.module’;
platformBrowserDynamic().bootstrapModule(AppModule);
{% endcodeblock %}
接著比較 app.module.ts
可以看到 Angular 在 起始模組(AppModule) 中透過 bootstrap
屬性指定了 AppComponent 為起始元件。
{% codeblock app.module.ts lang:ts %}
// [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 { }
{% endcodeblock %}
而 Ionic 看起來似乎也透過 bootstrap
屬性指定了 IonicApp,不過仔細看一下 IonicApp 是來自 ionic-angular
,查看一下它的原始碼,似乎 IonicApp 也不算是一個 Angular Component,而在 imports
屬性上發現 MyApp 這個 Component 不是直接被加入,而是多個 IonicModule.forRoot(MyApp)
將它註記成起始元件,我們可以 Ionic 的模組就此時開始介入。
開啟
src\app\app.component.ts
可以看到類別名稱就是 MyApp,在 Angular 專案內預設會是叫做 AppComponent。
因為目前並沒有深入研究IonicModule.forRoot
背後運作機制,所以不確定將它解釋成註記是否正確,目前只能確定 Ionic 起始內容是此方法所指定的 Component。
{% codeblock app.module.ts lang:ts %}
// [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 {}
{% endcodeblock %}
在 Angular 中透過 @Component 這個裝飾器為 AppComponent 加入了選擇器屬性-selector
,Angular 會自動將網頁內含有該選擇器的 tag 替換成 AppComponent 的樣板。
{% codeblock app.component.ts lang:ts %}
// [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 {
}
{% endcodeblock %}
切換到 Ionic 可以發現 MyApp 的 @Component 裝飾器竟然沒有設定 selector
屬性,由剛才的 AppModule 說道 IonicModule.forRoot(MyApp)
的這個方法,可以知道 Ionic 是透過直接明確指定類別的方式宣告起始元件。
{% codeblock app.component.ts lang:ts %}
// [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();
});
}
}
{% endcodeblock %}
眼尖的人其實可以發現 Ionic 預設在 AppModule 內比 Angular 專案多設定了
entryComponents
屬性。
查詢 Angular 官網的 Dynamic Component Loader 可以看到說明,但是說明感覺很籠統,目前看起來應該是說只要是透過ViewContainerRef.createComponent()
來建立的元件都需要註冊到entryComponents
。
從建構式內還可看到
splashScreen.hide();
,Ionic 也提供給我們可以設定程式啟動的等待畫面。statusBar.styleDefault();
則是讓我們設定手機最上方的狀態列,例如我們希望 App 佔滿整個螢幕時可以把狀態列隱藏起來。
開啟 Angular 專案 src\app\app.component.html
,我們可以看到最後一行也是最重要的一行 <router-outlet></router-outlet>
,Angular 的 路由模組(RouterModule) 就是透過這個 路由插座(router-outlet
) 來插入符合路由規則的元件。
{% codeblock app.component.html lang:html %}
// [Angular] src\app\app.component.html
{% endcodeblock %}
再來看 Ionic 專案可以發現預設並沒有加入 路由模組,更沒有類似 app-routing.module.ts
可以設定路由規則的檔案,開啟 src\app\app.html
可以看到 Ionic 改用自己的導覽元件-ion-nav
,並將 root
屬性指定給 rootPage
變數。
{% codeblock app.html lang:html %}
// [Ionic] src\app\app.html
<ion-nav [root]=”rootPage”>
{% endcodeblock %}
再回過頭看看 app.component.ts
,rootPage
變數被設定成 TabsPage Component。
從上面我們可以概略知道以往我們在 Angular 上很習慣的會利用路由模組來作頁面切換(Angular 屬於 SPA 程式,這邊的頁面不是只整個 HTML 版面,而是指主要操作區域),而在 Ionic 上 卻是透過 ion-nav
來處理。
那 ion-nav
是什麼?
{% blockquote Ionic http://ionicframework.com/docs/api/components/nav/Nav/ Nav %}
ion-nav is the declarative component for a NavController.
{% endblockquote %}
在查詢官方文件可以知道 NavController 是導航控制器組件的基底類別,目前 Nav 和 Tab 2個 Component 都繼承自它-NavControllerBase。
查詢 NavControllerBase 程式結構,我們可以看到一些導覽方法,例如:push
、pop
、insert
、remove
。
{% codeblock nav-controller-base.d.ts lang:ts %}
// [Ionic] node_modules\ionic-angular\navigation\nav-controller-base.d.ts
import { ComponentFactoryResolver, ComponentRef, ElementRef, ErrorHandler, EventEmitter, NgZone, Renderer, ViewContainerRef } from ‘@angular/core’;
…
/**
{% endcodeblock %}
其實這樣看起來 Nav(ion-nav
) 與 Tab(ion-tab
) 與 Angular 的 路由插座(router-outlet
) 相比更加強大,因為它們本身就提供的頁面切換的方法,可以將它本身的 content 內容替換成我們指定的元件類別,當然這都是因為它們繼承了 NavControllerBase。
與 Angular 一樣我們可以透過下列指令協助我們快速建立各種類型檔案:ionic generate [<type>] [<name>]
generate
可以縮寫成g
。
Ionic CLI 所提供的類型有:component
, directive
, page
, pipe
, provider
, tabs
,其中 page
與 tabs
是 Angular 所沒有的,所以我們就先建立一個 page
試試,指令如下:ionic g page page1
這邊筆者覺得 Angular CLI 做得比較好,因為 Angular CLI 會標示出建立那些檔案、修改那些檔案,Ionic CLI 目前只有一行成功訊息,這對於剛接觸的人可能會比較不親合。
我們可以看到 Ionic CLI 會依類型建立資料夾,並將新建的類型檔案放置到對應的資料夾下。Angular CLI 則是統一放置在 src\app\
下面。
接著我們開啟 src\pages\page1\page1.ts
,我們可以看到熟悉的裝飾器-@Component
,也就是說 page
本質上還是 Angular Component。
{% codeblock page1.ts lang:ts %}
// [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’);
}
}
{% endcodeblock %}
比較一下可以發現 Ionic Page 多了一些東西:
NavController
參數:我們已經知道 Ionic 是透過導覽模式來切換頁面,所以 Ionic CLI 貼心透過依賴注入(Dependency Injection,簡稱DI)的幫我們將 NavController 加到頁面類別內。
我們可以看到 constructor 內的參數都多了存取修飾詞-
public
,此為 TypeScript 的語法糖,我們可以想成它會自動在 class 內建立一個同名的全域變數,並將值指向該變數,我們在其他方法內就可以直接透過this.navCtrl.xxx
來使用 NavController。
ionViewDidLoad()
:就像我們透過 Angular CLI 來建立 Component 時它會幫我們增加 ngOnInit()
方法一樣,這些方法都牽涉到生命週期。
查詢官方網站可以看到 NavController Lifecycle events。
@IonicPage()
裝飾器:開啟其他預先建立好的 page 可以發現既沒有 Module 檔案,class 內也沒加上 @IonicPage()
裝飾器。
反過來看,這些預設的 page 卻跟 Angular Component 一樣被加入到 AppModule 的 declarations
,也同時設定到 entryComponents
屬性。
Angular 的 Component 必須註冊到 NgModule 內才可以使用。
page1.module.ts:開啟 src\pages\page1\page1.module.ts
可以看到這是一個 Angular Module,
而 Page1Page 被註冊到這個 NgModule 內。
{% codeblock page1.module.ts lang:ts %}
// [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 {}
{% endcodeblock %}
接下來我們在 Home 的樣板上面加入一個按鈕,並在其點擊事件內透過 NavController 導覽至 Page1,開啟並編輯 src\pages\home\home.html
。
{% codeblock home.html lang:html %}
// [Ionic] src\pages\home\home.html
Take a look at the src/pages/
directory to add or change tabs,
update any existing page or create new pages.
{% endcodeblock %}
接著編輯 src\pages\home\home.ts
,要注意的是傳給 navCtrl.push
的參數是 'Page1Page'
字串而不是 Page1Page
類別。
{% codeblock home.ts lang:ts %}
// [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’);
}
}
{% endcodeblock %}
透過 ionic serve
來啟動專案,從瀏覽器上看頁面導覽效果已經出現了。
做到這般看似正常,但是有點怪怪的,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
。
NgModule 內除了註冊 Component 外,還需再 imports
屬性內加入 IonicPageModule.forChild(<component>)
。
NavController 透過 字串(string) 來指定要導覽到該頁面,如果將字串改成 Component 的類別執行導覽時就會出現錯誤。
反過來說如果我們不要使用延遲載入的話要如何調整,如同剛剛修改的 home.ts
。
{% codeblock home.ts lang:ts %}
// [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);
}
}
{% endcodeblock %}
src\app\app.module.ts
修改如下,再重新執行專案,應該就會正常。{% codeblock app.module.ts lang:ts %}
// [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 {}
{% endcodeblock %}
declarations
以及 entryComponents
屬性內,將 src\app\app.module.ts
修改如下,再重新執行專案,應該同樣可以正常運作。{% codeblock app.module.ts lang:ts %}
// [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 {}
{% endcodeblock %}
其實如果仔細觀察,沒有延遲載入時頁面切換會有滑動效果,反之則沒有,在設計上這一點可能也要考量一下,例如操作功能的切換頁面可能透過延遲載入比較適合,因為我們大部分會透過 tabs 或是 menu 選單來做切換,但是同一個功能內的頁面切換不要透過延遲載入比較適合,因為上下頁面切換的滑動效果會讓整個功能比較有整體感。
我們先修改 page1 樣板,加入 input 輸入方塊,一個導覽按鈕,點擊時會再次導覽回 page1,一個導覽返回鈕,接著在類別內加入一個 page_length 屬性並給予 NavController 目前頁面堆疊數量,最後再透過嵌入繫結方式繫結到樣板上。
Ionic 的 Component 都已經預先載入了,所以可以直接使用。
{% codeblock page1.html lang:html %}
// [Ionic] src\pages\page1\page1.html
<ion-navbar>
<ion-title>page1</ion-title>
</ion-navbar>
{% endcodeblock %}
{% codeblock page1.ts lang:ts %}
// [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();
}
}
{% endcodeblock %}
啟動專案並透過瀏覽器操作。
由上面的操作,我們可以看到我們每次呼叫 push
方法時,Ionic 會在產生一個全新的 page1 並且取代目前頁面,從 page_length
屬性也可以印證 NavController
紀錄的頁面確實增加了,當呼叫 pop
方法時,可以發現 Ionic 會移除目前頁面並顯示上一頁面內容,我們也可以看到之前頁面的內容都會被保留下來。
我們可以假想 NavController
就像一個大箱子,每次 push
時就會放入一本新書,每次 pop
時就會取出最上面的一本書,我們能看得的書永遠是最後一本,因為它會被放在最上層,下層的書都會被遮住,網頁上看到的 page 也是一樣是最後載入的 page,所以我們可以說這是一種後進先出(Last In First Out)的堆疊模式,最後進去的會最先出來。
了解這種模式之後,可以發現它會衍生出一些問題:
占用記憶體:每個頁面都需要用到記憶體來保存狀態,因此如果堆疊越多層也代表記憶體占用越多。
重複的頁面:我們在某個頁面更新資料後,在後續導覽過程不小心導覽到比較舊的歷史頁面,對使用者來說會以為資料沒更新成功。
因此在設計上可以參考一些網站導航的設計原則,例如階層不要太多可能3~5層,不同功能盡量避免可以直接切換,讓導覽途徑是樹枝展狀的階層圖,而不是交錯複雜的網狀圖。
{% img /images/download.png 36 %}ionic-first-app_2017-09-19.zip