Ionic 2 应用剖析
0 开始之前
通过本教程之前,您应该至少了解一些基本的Ionic 2概念。您还必须已经安装了Ionic 2 在您的机器上。
1 创建一个新的Ionic 2 应用
我们将使用有Ionic团队创建的tutorial模板,可见于官方教程,来创建我们的应用程序。要做到这一点,您需要运行以下命令:
ionic start ionic2-tutorial tutorial --v2
现在您的应用程序将自己开始建立。为运行后续的命令,你应当将项目目录作为当前工作目录:
cd ionic2-tutorial
简单瞟一眼应用效果,使用serve命令:
ionic serve
上面也说了,这些命令应该在当前项目目录下执行。
2 目录结构
如果你看看生成的文件和文件夹,这一切看起来非常类似于一个Ionic 1最初的应用程序。这也是一个非常典型的科Cordova风格项目结构。
如果你再看看在src 文件夹,事情开始看起来有点不同:
通常在一个Ionic 1应用程序中,人们所有的Javascript文件(控制器、服务等)在一个文件夹中,所有的模板在另一个文件夹,然后所有的样式包含在一个app.scss文件中。
Ionic 2应用程序的默认结构通过功能的组织,因此一个特定组件(在上面的示例中我们有一个基本的页面组件,组件列表,和一个项目详细信息组件)的所有逻辑、模板和样式都在一起。这是Angular 2方法论的完美应用,一切都是独立的组件,这些组件可以很容易地在其他地方或项目中重用。如果你想重复使用一个特定的功能,或有很多人工作在同一个项目中,旧的Ionic 1方法会变得非常麻烦。
根据功能组织代码的想法不是Angular 2 & Ionic 2 的特权,事实上人们在Ionic 1中使用和倡导基于特征的方式,只是大多数人没那样做(趋势是很难打破)。通过Angular 2 的工作方式,默认就使用基于特征的结构,因此不难推行这种结构。
index.html
已经是惯例了,浏览器第一个打开的文件就是 index.html 。因此我们先来看看Ionic 2中是怎样的:
<!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="assets/manifest.json"> <meta name="theme-color" content="#4e8ef7"> <!-- 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.log('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> <!-- cordova.js required for cordova apps --> <script src="cordova.js"></script> <!-- The polyfills js is generated during the build process --> <script src="build/polyfills.js"></script> <!-- The bundle js is generated during the build process --> <script src="build/main.js"></script> </body> </html>
这看上去非常简洁,和Ionic 1没有什么不同。这里最大的不同是没用附加ng-app 到body标签(目的是是让Ionic知道应用存在的地方),而是使用了:
<ion-app></ion-app>
根组件将在这里被创建,通常你的入口应用在这里注入。cordova.js的引用让我们可以使用Cordova创建应用(将应用打包为native应用,可以提交到App Store),polyfill.js是为浏览器某些特点功能的基本补丁,main.js是我们应用绑定的代码。
基本上,这看起来就是一个非常普通的网页。
assets
这个assets目录用于保存你工程里面使用的静态文件,就像图片、JSON数据文件等等。任何这个文件夹下的东西都会在应用程序每次build编译时覆盖拷贝到你的build目录。
theme
这个 theme 目录包含了你应用程序中的 global.scss 和variables.scss 文件。多数你应用中的样式是通过使用每个组件自己的 .scss 文件,但是你可以使用 global.scss 文件定义任何自定义样式,通过不同的方式,你也可以修改 variables.scss 文件中的 SASS 变量来修改你应用的样式。
app
所有的Ionic 2 App 都有 root component。这不是和你应用里面其他组件的差别,一个明显的差别是它在自己的 app 文件夹中,而且被命名为 app.component.ts。
如果你有一点不确定 component 到底是个什么东西,我们具体来看看:
@Component({ templateUrl: 'my-component.html' }) export class Something { // ...snip }
这就是一个具有 something 功能的组件,技术上来说component关联一个视图,否则这个类可能考虑为services更好。不管是component还是servece,创建都差不多,都可以被导入import到你的应用中。
根组件root component是第一个被加载的,接下来我们看看root component是怎么定义和工作的。我们先看看整个文件,然后分解说明:
import { Component, ViewChild } from '@angular/core'; import { Platform, MenuController, Nav } from 'ionic-angular'; import { StatusBar } from 'ionic-native'; import { HelloIonicPage } from '../pages/hello-ionic/hello-ionic'; import { ListPage } from '../pages/list/list'; @Component({ templateUrl: 'app.html' }) export class MyApp { @ViewChild(Nav) nav: Nav; // make HelloIonicPage the root (or first) page rootPage: any = HelloIonicPage; pages: Array<{title: string, component: any}>; constructor( public platform: Platform, public menu: MenuController ) { this.initializeApp(); // set our app's pages this.pages = [ { title: 'Hello Ionic', component: HelloIonicPage }, { title: 'My First List', component: ListPage } ]; } initializeApp() { this.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(); }); } openPage(page) { // close the menu when clicking a link from the menu this.menu.close(); // navigate to the new page if it is not the current page this.nav.setRoot(page.component); } }
1.imports
import { Component, ViewChild } from '@angular/core'; import { Platform, MenuController, Nav } from 'ionic-angular'; import { StatusBar } from 'ionic-native'; import { HelloIonicPage } from '../pages/hello-ionic/hello-ionic'; import { ListPage } from '../pages/list/list';
最开始,有一些imports定义。我们用于加载其他组件或服务到这个组件。所有的组件,除了根组件,你都可以看到类定义是这样的:
export class Something { }
非常简单,我们 expoet 组件就是为了能够在其他地方 import。在这个例子里面,我们从 Ionic 库导入了 Platform, Nav和 MenuController 服务。 Platform 提供了关于运行应用程序平台的信息, Nav 提供应用里面导航的引用, MenuController 允许我们提供控制菜单。
我们从Angular 2导入 Component 和 ViewChild 。 Component 几乎无处不在,因为我们用于创建组件, ViewChild 用于获取组件中元素的定义。
我们也导入 importing 了 我们自己创建的HelloIonicPage 和 ListPage 组件。 这些组件定义在 src/pages/hello-ionic/hello-ionic.ts 和 src/pages/list/list.ts (根据 import 语句对应的路径)。注意我们没有包含src路径在import中,因为是当前文件的相对路径,而我们已经在src目录中。因为我们在名为app的子文件夹中,所以我们到上级目录使用../。
接下来我们看到从ionic-native导入 StatusBar,因为我们通过Ionic2使用Cordova来访问本地功能,就像控制 status bar。Ionic Native是由Ionic提供的服务以便于方便使用Cordova插件。尽管你不用为了使用Ionic Native而包含Native functionatilty,你可以直接使用Cordova插件。
2. Decorator
Decorators,就像 @Component 和 @Directive,通过使用在类定义上添加元数据(扩充信息)给我们的组件,看看我买的 root component:
@Component({ templateUrl: 'app.html' })
这里我们使用 templateUrl 让组件知道使用哪个文件作为视图 (你也可以使用 template 作为内联模版而不是 templateUrl)。
3. Class 定义
之前的所有都没有真正的做一些功能,只是一个设置和搭建。现在我们要开始定义一些行为,来看一看吧:
export class MyApp { @ViewChild(Nav) nav: Nav; // make HelloIonicPage the root (or first) page rootPage: any = HelloIonicPage; pages: Array<{title: string, component: any}>; constructor(public platform: Platform, public menu: MenuController) { this.initializeApp(); // set our app's pages this.pages = [ { title: 'Hello Ionic', component: HelloIonicPage }, { title: 'My First List', component: ListPage } ]; } initializeApp() { this.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(); }); } openPage(page) { // close the menu when clicking a link from the menu this.menu.close(); // navigate to the new page if it is not the current page this.nav.setRoot(page.component); } }
首先我们定义一个新类MyApp,classes是ES6的新特性。
我们传入一些参数到构造函数constructor:platform 和menu 然后它们的类型是 Platform 和MenuController。这样我们通过构造函数注入inject了这些服务(比如MenuController 将作为菜单),通过使用public关键字使得作用域在整个类;意味着我们可以通过this.menu 或者 this.platform在这个类里面的任何地方访问它们。
The Platform service提供了程序所运行平台的相关信息 (例如:宽高、横竖、分辨率等),这里我们用来判断app是否就绪。
MenuController服务允许我们创建和管理一个滑动菜单。
在构造函数的上方,我们也定义了几个成员变量用于保存我们类里的rootPage 和 pages。通过在构造函数上面定义,我们就可以在整个类里通过this.rootPage或 this.pages来使用。
我们定义 rootPage 为 HelloIonicPage 组件,作为首先显示的第一页(你也可以简单的改变它,用ListPage代替)。
构造函数之外,我们定义了一个名为 openPage 的方法,传入一个page参数,通过调用setRoot方法设置为当前页。注意,我们获取this.nav引用通过一种奇怪的方式。通常,我们导入NavController 使用与 MenuController 和Platform 同样的方式然后调用它的 setRoot,但是你不能从根组件调用它,作为替换我们获取引用通过Angular2提供的@ViewChild。
一个初学者特别困惑的事是这样:
rootPage: any = HelloIonicPage; pages: Array<{title: string, component: any}>;
之前提到过,这里是创建成员变量。但是你可能会想Array<{title: string, component: any}>是什么鬼。你应该知道,Ionic 2使用TypeScript,这些鬼就是types(类型)。类型简单的说就是“这些变量应该只含有这些类型的数据”。这里,我们可以说rootPage可以包含any类型的数据,pages仅可以包含数组,而这些数组仅可以包含由字符串标题和any类型component组成的对象。这是一个非常复杂的类型,你可像下面这样简单处理:
rootPage: any = HelloIonicPage; pages: any;
或者你也可以完全不用类型。使用类型的好处是给你的应用程序增加了错误检查和一个基础水平的测试——如果你的pages数组被传入了一个数字,那么你的应用将被中断,而这将直观的去了解和处理。
Root Components 模版
当我们创建根组件是我们提供了一个模版给组件,就是被渲染到屏幕的内容。1).这里是我们在浏览器运行时根组件的样子:
现在我们稍微详细的看看模版HTML。
<ion-menu [content]="content"> <ion-header> <ion-toolbar> <ion-title>Pages</ion-title> </ion-toolbar> </ion-header> <ion-content> <ion-list> <button ion-item *ngFor="let p of pages" (click)="openPage(p)"> {{p.title}} </button> </ion-list> </ion-content> </ion-menu> <ion-nav [root]="rootPage" #content swipeBackEnabled="false"></ion-nav>
先看下第一行:
<ion-menu [content]="content">
这是menu元素的content 的属性为 content。记住这里的 “content” 是表达式而不是字符串。我们不是设置 content 属性为字符串“content”,我们设置的是变量 “content”。如果你跳到文件底部你就会看到:
<ion-nav id="nav" [root]="rootPage" #content swipe-back-enabled="false"></ion-nav>
上面代码通过添加#content,我们就创建了一个名为content的变量指向这个组件,也是menu的content属性使用的变量。所以,menu将使用<ion-nav>作为它的主要内容。这里我们设置root属性为我们在类中定义(app.ts)的rootPage。
接下来我们看看是什么有意思的东西:
<button ion-item *ngFor="let p of pages" (click)="openPage(p)"> {{p.title}} </button>
在这一小块代码中挤进了Angular 2的语法。为构造函数中定义的每一个页面创建一个按钮,号语法意味这它将为每个页面创建一个嵌入式模版(它不会在DOM中渲染出上面的代码,而是使用模版创建),通过使用*let p我们可以获取到某个特定页面的引用,用于点击事件时传递到openPage方法(在根模块中定义的)。回过头去看看openPage方法可以看到这个参数用于设置rootPage**:
this.nav.setRoot(page.component);
App Module
我们已经覆盖了一些根模块的细节,但是这里还有一个名为app.modules.ts的神秘文件在app目录下。
为了在我们的程序中使用页面和服务,我们需要把它们添加到 app.module.ts文件。我们创建的所有页面需要被添加到 declarations 和 entryComponents 数组,所有服务需要被添加到providers数组,所有自定义的组件或pipes只需要被添加到declarations数组。
你还会发现main.dev.ts 和 main.prod.ts 文件在同一个目录下面。其中只有一个会被用到(取决于你是开发还是发布的build)。实际上它负责启动您的应用程序(这个意义上它有点像index.html)。它将导入app module并启动应用程序。
页面
根组件是一个特例,我们通过 ListPage组件来看看如何添加一个普通的视图到一个Ionic2应用程序。你能看到这个页面,通过选择应用程序中的“My First List”菜单,来查看这个页面:
代码是酱紫的:
import { Component } from '@angular/core'; import { NavController, NavParams } from 'ionic-angular'; import { ItemDetailsPage } from '../item-details/item-details'; @Component({ templateUrl: 'list.html' }) export class ListPage { selectedItem: any; icons: string[]; items: Array<{title: string, note: string, icon: string}>; constructor(public navCtrl: NavController, public navParams: NavParams) { // If we navigated to this page, we will have an item available as a nav param this.selectedItem = navParams.get('item'); this.icons = ['flask', 'wifi', 'beer', 'football', 'basketball', 'paper-plane', 'american-football', 'boat', 'bluetooth', 'build']; this.items = []; for(let i = 1; i < 11; i++) { this.items.push({ title: 'Item ' + i, note: 'This is item #' + i, icon: this.icons[Math.floor(Math.random() * this.icons.length)] }); } } itemTapped(event, item) { this.navCtrl.push(ItemDetailsPage, { item: item }); } }
和根组件一样,我们有一些 import 语句,然后我们同样有 @Component 修饰符:
@Component({ templateUrl: 'list.html' })
然后是这样的:
export class ListPage { }
这里有个前缀 export 而在根组件中没有。这允许我们的页面组件在其他地方被导入(import)。
这个视图中有个叫 NavParams 的组件通过构造函数加了进来。在导航的时候我们就可以返回这个视图的详细信息,我们先查一下值:
this.selectedItem = navParams.get('item');
这时是undefined,因为这个页面被设置成了rootPage(在根组件中通过openPage方法设置),我们没用通过navigation stack导航到这个页面。
Ionic 2 中,如果你想添加一个视图,并且保存页面导航历史随时可以返回,那么你需要push这个页面到n
navigation stack,对应的移除用pop。
在 ListPage 组件中,我们通过 itemTapped 方法(ListPage 模版中,但某条记录被点击时触发) push 了 ItemDetailsPage :
itemTapped(event, item) { this.navCtrl.push(ItemDetailsPage, { item: item }); }
上面push 了 ItemDetailsPage 组件到 navigation stack,使之成为当前活动视图,然后把被点击的item传入详情页中。
现在我们看看细节页面 ItemDetailsPage 组件的细节,我们可以使用 NavParams 获取传入记录的细节,好像这样(毕竟我们push了些东西到navigation stack):
this.selectedItem = navParams.get('item'); console.log(this.selectedItem);
这就是Ionic2主从复合的基本模式了。
还有就是记住,你可以通过命令行轻松创建页面:
ionic g page MyPage
这将自动创建你需要的页面文件。
总结
毫无疑问Ionic 2和Angular 2 取得了巨大的进步在组织结构和性能上,但他们看起来也很吓人。尽管最初似乎需要很多学习和面对困扰,但我认为它很有意义。