互联网+智慧城市:核心技术及行业应用
上QQ阅读APP看书,第一时间看更新

2.4 Ionic(Angular/Cordova)框架

2.4.1 概述

如何创建移动互联网的客户端核心要素移动App?一般而言,一个开发者需要学习和掌握每一个平台下的语言:如果要编写苹果iOS应用,那么要掌握Objectie-C或者Swift;如果要编写Android应用,则需要学习Java。如果有一种方法可以只学习一种语言就能在多个平台下使用,那就好了。答案是肯定的:通过使用Web通用语言和某些神奇的框架,开发者能够在开发应用时“一次编码部署到多个移动平台”,这就是所谓的“混合移动App”,因为它是用Web技术开发的,但融合了移动设备的原生能力。

什么是混合移动应用?和传统的原生移动应用不同,它不使用设备原生开发语言进行开发。混合移动App是用Web技术(HTML、CSS和JavaScript)开发的。上节介绍的jQuery Mobile以及本节将重点介绍的Ionic是当今两个最流行的混合移动App开发框架。

什么是Ionic框架呢?简单来说,它是一个用HTML、CSS和JavaScript构成的用户界面框架,专门用于混合移动App的开发。除了用户界面组件,Ionic框架也包含一个强大的命令行接口CLI和一套附属服务,如Ionic View和Ionic Creator。

Ionic实际上融合了多种技术,整个技术栈的最上层是Ionic框架自身,它提供了移动应用程序的用户界面层,接下来一层是Angular,这是一个极其强大的Web应用框架。这些框架的下面是Apache Cordova,它允许Web应用程序调用设备原生能力并将App转换成原生App。这些技术融合在一起,使Ionic变成了一个创建混合移动App的强大平台。下面对这三个技术做简单介绍并在后续章节进行详细介绍。

(1)Ionic框架

Ionic框架的主要功能是提供了Web App开发中所不具备的UI组件。例如,在许多移动App中都有一个UI组件Tab Bar(就是现在很多App所具备的底部菜单按钮),但这个组件在原生的HTML标签中根本不存在。Ionic框架扩展了HTML库并提供了一个这样的组件。这个组件是由HTML、CSS和JavaScript编写的,它的每个行为和外观都被重写了,和原生控件一模一样。Ionic还有一个CLI工具,用于轻松创建、编译和发布Ionic应用程序。Ionic平台还扩展了几个附属功能,包括一个在线的图形用户界面的构造工具,用于图形化方式设计Ionic App、打包及更新。

目前Ionic已经是第三版,该版本是一次传统意义上的常规升级,而不是像Ionic1到Ionic2那样的重大升级,Ionic2除了框架自身的巨大改变外还有一个重大的变化就是底层技术的变化,即Angular。Ionic2基本上是一个全新的框架,这个框架的主要改进如下。

1 全新的导航:彻底控制App的导航体验,不再局限于URL bar。

2 原生支持:支持更多的原生功能,能轻松调用设备提供的全部能力,不需要到处搜索扩展插件和代码。

3 强大的主题:通过全新的主题系统,马上就能轻松地将你的品牌颜色和设计进行匹配。

4 材料设计:为Androiid App提供完整的材料设计支持。

5 Windows Universal App:支持开发运行在Windows Universal平台上的App。

Ionic框架的主要目标是UI层,它通过集成Angular和Cordova的方式来提供接近原生的体验。

(2)Angular

Ionic技术栈的下一层是Angular,这是一个由谷歌支持的开源项目。自2009年开始发布以来,Angular已经变成最流行的Web应用框架。Angular的目的是提供一个用于建造复杂的、单页面Web App的MVW(Model View Whatever)框架。Ionic团队决定利用Angular框架提供的强大功能,因此将其集成到Ionic中。例如,Ionic的定制UI组件其实就是Angular组件。随着Angular2的推出,框架也进行了大量改进。

(3)Cordova

Ionic技术栈的最后一层是Apache Cordova。Cordova起源于Nitobi Software公司于2009年所开发的一个开源项目,能够利用Web技术来构建嵌入WebView的原生App。2011年,Adobe System收购了Nitobi(包括PhoneGap这个商标),这个项目不得不重新命名。最终这个项目的版本被重命名为Cordova。Cordova提供了WebView和设备原生层之间的接口,提供了一个桥接框架,弥补这两个技术之间的空隙(这就是原名PhoneGap的来由)。由于许多功能是以插件系统的形式提供的,这种形式使得核心库保持较小规模。除了Android和iOS两个移动平台之外,Cordova还支持其他移动平台,例如Windows Phone、Blackberry和FireOS。除了库本身,Cordova还拥有自己的命令行工具,用于搭建框架、编译和部署你的移动应用。Ionic CLI就是基于Cordova CLI构建的。

2.4.2 安装

Ionic开发环境的安装看似复杂,其实非常简单,一次基本的安装,只需要以下4个步骤。

(1)安装Node.js

Ionic是基于Node.js构建的,Node是一个能够让你在浏览器之外运行JavaScript的平台。安装Node时,我们需下载安装包并安装(笔者下载的是node-v8.9.4-x64),安装后打开终端并输入node-v;这个命令会打印Node当前安装的版本号。

C:\Users\13701>node -v
v8.9.4

同时你需要确认NPM(一个Node模块的包管理器)是否更新。运行下面命令查看NPM安装版本。

C:\Users\13701>npm -v
5.6.0

(2)安装Git

Git是一个版本控制工具,Ionic CLI是使用Git来管理模板的。下载并安装Git,笔者下载并安装的是:Git-2.16.1.4-64-bit;安装完成选择打开git bash命令行工具,输入以下命令进行测试安装成功。

$ git --version
git version 2.16.1.windows.4

(3)安装Cordova CLI

打开Git Bash,通过NPM安装,输入以下命令。

$ npm install -g cordova

等待一段时间后,出现如下提示。

C:\Users\13701\AppData\Roaming\npm\cordova -> 
C:\Users\13701\AppData\Roaming\npm\ node_modules\
cordova\bin\cordova
+ cordova@8.0.0
added 408 packages in 96.045s

这表示已经安装成功,我们还可以输入以下命令来验证。

$ cordova -v

如果出现下述cordova版本信息,则表示安装成功了。

You have been opted out of telemetry. To change this, run: cordova telemetry on. 
8.0.0

(4)安装Ionic CLI

打开Git Bash,通过NPM安装,输入以下命令。

  $ npm install -g ionic

等待一段时间后,出现如下提示。

C:\Users\13701\AppData\Roaming\npm\ionic ->
C:\Users\13701\AppData\Roaming\npm\node_modules\
ionic\bin\ionic
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.1.3 (node_modules\ionic\
node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.1.3: 
wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ ionic@3.19.1
added 253 packages in 238.391s

这表示已经安装成功,我们还可以输入以下命令来验证。

$ ionic–v

如果出现下述Ionic版本信息,则表示安装成功了。

3.19.1

2.4.3 新建Ionic项目

Ionic CLI提供了一个简单的命令,允许你创建一个ionic项目:ionic start。这个命令会在当前目录下创建一个基本的Ionic应用。Ionic框架通过一系列初始模板来创建一个项目的脚手架。这些模板包含一个指定的模板(Ionic有三种模板:blank白板、sidemenu侧菜单和tabs底部菜单标签页)、一个Github库、一个Codepen或者一个本地目录。运行下列命令来创建一个Ionic项目。

  $ ionic start myApp tabs --cordova

该命令指定使用tabs模板,并且集成Cordova框架。创建完成后将生成myApp目录,如图2.9所示,我们需要切换到该目录:$cd myApp

在myApp项目目录下,使用Ionic CLI内置命令:$ionic serve。

这会启动一个简单的Web服务器,并打开你的浏览器,自动加载App,如图2.10所示,我们会在浏览器中看到Tab风格的Ionic App。

到此为止,一个最简单的App就生成了。

图2.9 Ionic创建的项目myApp的目录结构

图2.10 Ionic项目myApp运行结果

2.4.4 Angular和TypeScript基础

1. Angular2是什么

本书中所述的Angular是目前大家通常称为Angular2的版本,以和其第一代版本Angular1(AngularJS)区别。Angular2是一个全新框架,只是借用了原来的名字和名义而已。在2016年的ngConf大会上,Angular团队发布RC1版本,这个框架只使用Angular而不是Angular2命名。因此,本书中我们遵照这种命名,将Angular2称为Angular。

(1)组件

从Angular1到Angular2的一个最大改变就是不再依赖scope、controller或者某种程度的directive了。Angular团队使用基于组件的方法构建元素及其相关逻辑。下面是一个Angular组件的例子。

  import { Component } from'@angular/core'
  @Component({
    selector:'my-first-component',
    template:`<div>Hello, my name is {{name}}.
    <button (click)="sayMyName()">Log my name</button></div>`
  })
  export class MyComponet {
   constructor() {
      this.name='Inigo Montoya'
    }
   sayMyName(){
    console.log('Helllo. My name is',this.name, '.
          You killed my father. Prepare to die.')
    }
   }

这和Ionic创建自己的组件库是一样的方式。事实上,你可以用自己的定制组件随意扩展Ionic应用程序,没有任何限制。

首先,第一句代码从Angualr库中导入Component模块。在Angular中,这是通过依赖注入进行的。

接着,我们用@Component装饰器为代码添加一些元数据给编译器。我们决定使用一个自定义的HTML选择器。这样,只要我们一使用<my-first-component></my-first-component>标签,对应的模板就会插入DOM中。模板有两种风格:一种是像上面代码中一样的行内风格;另一种是外部引用。如果你的模板代码为了阅读方便需要排列成多行,请使用反引号(`),而不是单引号(')括住模板代码。

在装饰器之后,我们导出了类的定义,即MyComponent。在类的构造函数中,我们将name变量赋值为“Inigo Montoya”。

最后,这个类还有一个公共方法sayMyName,这个方法会向控制台输出一段文字。

(2)输入

由于Angular是基于组件模型构建的,它需要有一种将信息传递到组件的机制。这是由Input模块来负责处理的。我们来看一个简单的组件<current-user>的例子,为了使用这个组件,需要一个user参数。这个标签使用起来如下所示。

<current-user [user]="currentUser"></current-user>

而这个组件的定义如下所示。

  import {Component, Input} from'@angular/core';
  @Component({
    selector: 'current-user',
    template: '<div>{{user.name}}</div>'
   })
   export class UserProfile{
   @Input() user;
   constructor() {}
   }

在类的定义里面,有一个@Input绑定到user变量。通过这种绑定,Angular会传递currentUser变量给这个组件,从而让模板渲染出user.name值。这就是Ionic组件强大的地方。我们可以用这个例子中的机制来进行数据的传递和参数的设置。

(3)模板

模板是一个HTML片段,由Angular将指定元素和属性组装在一起构成的动态内容。

1 []绑定属性:当组件需要解析和绑定一个变量时,Angular会用[]语法。本节在讲输入时接触过。如果我们的父组件中有一个this.currentColor,要将这个传递给子组件,Angular会保证这个值始终是最新的值。

<card-header [themeColor]="currentColor"></card-header>

2 ()事件处理:在Angular1中,我们会使用自定义指令来监听用户事件,例如元素的单击事件(有点像ng-click)。Angular中使用了一种更简单的方法,只需要将你想监听的子组件中的事件用圆括号括住,然后赋给父组件即可。

<my-componet (click)="onUserClick($event)"></my-component>

3 [()]双向数据绑定:默认地,Angular不会创建双向数据绑定。如果你需要用到双向数据绑定,新的语法也很简洁,将属性绑定语法和事件绑定语法结合起来。

<input [(ngModel)]="username">;组件的this.userName将始终和输入值保持同步。

4 *星号:在某个指定前使用星号会告诉Angular将我们的模板以指定的方式处理。例如,ngFor会把我们的<my-component>变成for each item in items。

<my-component *ngFor="let item of items"></my-component>

5 {{}}插值(渲染):是由一对双大括号{{}}组成,插值的变量上下文是组件类本身,插值是一种单向的数据流动:从数据模型到模板视图。

<div>Hello, my name is {{name}}</div>

(4)事件

Angular中的事件在模板中用圆括号进行标记,并触发某个组件类中的方法。例如,加入我们使用这个模板。

<button (click)="clicked()">click</button>

同时该组件的定义如下。

  @Component(…)
  Class MyComponent{
   Clicked() {
   }
  }

当按钮被单击时,这个组件的clicked()方法将被调用。另外,在Angular中,事件就像普通的DOM事件。它们能够向上冒泡并向下广播。

如果我们需要访问这个事件对象,可以将事件回调方法中简单传递一个$evenet参数。

  <button (click)="clicked($evenet)">click</button>

这个组件类应当变成如下。

  Class MyComponent {
   Clicked(event) {
   }
  }

1 自定义事件

如果你的组件需要向其他组件广播自定义事件怎么办?Angular中有一个很简单的办法。在我们的组件中,我们可以导入Output模块和EventEmitter模块,然后就可以用@Output装饰器定义自定义事件userUpdated了。这个事件是一个EventEmitter实例。

  import {Component, Output,EventEmitter} from'@angular/core';
  @Component({
    selector: 'user-profile',
    template: '<div>Hi, my name is</div>'
   })
   export class UserProfile{
   @Output() userDataUpdated=new EventEmitter();
   constructor() {
      
     This.userDataUpdated.emit(this.user);
    }
   }

当我们想触发事件的广播时,你可以在自定义事件上调用emit方法并传入一个参数,这个参数将随事件一起传递。

现在,我们可以在App中使用这个组件,并可以绑定user-profile发出的事件。

<user-profile (userDataUpdated)="userProfileUpdated($event)"></user-profile>

当我们将UserProfile组件导入另外一个组件后,它就能监听到userProfile Updated事件的广播了。

2 生命周期事件

Angular App和它的组件都有生命周期事件,允许开发者访问生命周期中的每个关键环节。这些事件通常和组件的创建、渲染、销毁相关。

A. NgModule。

Angular团队通过NgModule函数重新实现了App的引导方式。@NgModule使用元数据对象告诉Angular如何编译和运行模块代码。另外,@NgModule允许你将所有的依赖进行前置说明,而不用在App中声明多次。

import {NgModule} from'@angular/core';
import {BrowserModule} from'@angular/platform-browser';
import {AppComponent} from'./app.component';
@NgModule({
  imports:  [BrowseModule],
  declarations:[AppComponent],
  bootstrap: [AppComponent]
})
export class AppModule{ }

这段代码是一个基本的app.module.ts文件的例子,它使用了BrowserModule模块,这样Angular App才能运行在浏览器中,然后声明并启动AppComponent。

这个模块最终会被main.ts文件所用,并进行真正的引导过程。

  import {platformBrowserDynamic} from'@angular/platform-browser-dynamic';
  import {AppModule} from'./app.module';
  const platform =platformBrowserDynamic();
  platform.bootstrapModule(AppModule);

这段代码初始化了App即将运行的平台,然后用这个平台引导你的AppModule。Ionic starter模板会自动为你创建必要的模块。

B. 组件初始化事件。

当组件被创建时,它的构造器函数会被调用。在构造器中,我们必须在组件构造时进行初始化。但是,如果组件需要从子组件获得某些信息或属性,我们可能无法访问这些数据。

Angular提供了一个ngOnInit事件,这个事件会在组件初始化真正完成时触发。我们的组件可以在框架调用这个方法之前等待,然后就可以在组件中使用和解析所有的属性了。

C. 组件生命周期事件。

除了ngOnInit,还有ngOnDestroy:组件被销毁前调用;ngDoCheck:可以通过这个方法自定义变化监听;ngOnChanges(changes):组件的某个绑定值发生改变时该方法会被调用;ngAfterContentInit:这个方法会在内容第一次渲染前被调用一次;等等。

尽管可以使用上述Angular事件,但仍然建议使用Ionic事件。

(5)管道符

管道符,原先叫作过滤器,将一个值转换成新的值。除了自定义管道,Angular还提供了一些常用的管道操作,如Date、UpperCase、LowerCase、Currency、Percent等。

(6)@ViewChild

我们经常需要读写子组件的值或者子组件的方法。当父组件需要这些功能时,我们可以将子组件以一个ViewChild的形式注入父组件中。

  import {Component, ViewChild} from'@angular/core';
  import {UserProfile} from'../user-profile';
  @Component({
    template: '<user-profile (click)="update()"></user-profile>',
    directives: [UserProfile]
   })
   export class MasterPage{
   @ViewChild(UserProfile) userProfile:UserProfile;
   constructor() {
     this.userProfile.sendData();
    }
   }

从Angular Core中导入ViewChild和UserProfile组件。在@Component装饰器中,我们必须设置directives属性为注入的组件设置引用。构造器中包含了@ViewChild装饰器,在装饰器中将userProfile变量设置为注入的组件UserProfile。使用了这些代码后,我们就能调用UserProfile子组件的sendData方法了。

2. 核心概念

为了更好地理解复杂的Angular技术,本节对上一节进行细化和延伸。Angular官方文档列出了8个核心概念,分别是模块、组件、模板、元数据、数据绑定、指令、服务、依赖注入。从总览的角度看,各个概念在应用中所处的位置大概如图2.11所示。

图2.11 Angular的核心概念

借助图2.11,我们来大致解读一下这些核心概念。

1 与用户直接交互的是模板,模板并不是独立的模块,它是组成组件的要素之一。另一要素是组件类,用以维护组件的数据模型及功能逻辑。

2 模板是通过元数据指定的,元数据还包含很多其他的重要信息,这些信息用来告诉Angular如何去解释一个普通的类,如图2.11所示,元数据结合普通类而构成组件。

3 指令是Angular里的独立构成,它与模板密切关联,用来增强模板特性,间接扩展了模板的语法。

4 服务也是Angular里的独立构成,它是封装单一功能逻辑的单元,通常为组件提供功能扩展。

5 服务要能为组件所使用,通过依赖注入机制把服务引入组件内部,服务既可以单独注入某一组件,也可注入模块,两种注入方式使得服务的作用域不一样。

6 Angular的概念虽多,但理解起来并不难,这些概念中最重要的就是组件。纵观整个Angular应用,接收用户指令,加工处理后输出相应视图的过程中,组件始终处于这个交互的出入口,这正是Angular基于组件设计的体现。下面以组件为切入点,逐一揭开这些核心概念的面纱。

(1)组件、元数据及数据绑定

Angular框架基于组件设计,其应用由一系列大大小小的组件构成。下面以一个通讯录例子来说明,如图2.12所示。

图2.12 Angular组件的通讯录例子

所有框起来的部分均是由相应的组件所渲染,并且这些组件层层嵌套,自上而下构成组件树。如最外层的方框为根组件,包含了Header、ContactList以及Footer三个子组件,其中ContactList又有自己的子组件。举一个简单的Contact组件示例代码,如下所示。

  import { Component } from '@angular/core';
  @Component({
   selector: 'contact',
   template: '<p>张三'
  })
  class ContactComponent {
   constructor() { }
  }

可以看出Angular组件由两部分组成,@Component()和TS/ES6的类。TS/ES6类是处理组件的业务逻辑,而@Component()这部分称为装饰器,装饰器是TypeScript提供的一种语言特性,用来给类、函数等注入额外的信息,这些额外的信息实际上就是Angular核心概念——元数据。在Angular里,元数据主要以装饰器的函数参数指定。上例中定义了两个重要元数据selector和template,template顾名思义为模板,selector声明的是一个CSS3选择器,应用运行时匹配模板上的DOM元素,简单理解其实就是组件的标签名。

虽然每个组件各司其职,但组件以树的形式来组织意味着,组件不可能是孤立的存在,父子组件之间存在着双向的数据流动。每个组件均可以定义自己的输入输出属性,这些属性成为组件的对外接口,负责跟父组件进行交互。我们来完善Contact组件,添加输入输出属性,伪代码如下。

// import Component, Input, Output, EventEmitter, etc
@Component({
 selector: 'contact',
 template: '<p>张三'
})
export class ContactComponent {
 @Input() item: ContactModel; // 输入属性
 @Output() update: EventEmitter<ContactModel>; // 输出属性
 constructor() { }
 modify() {
  // ...
  this.update.emit(newValue);
 }
}

@Input()和@Output装饰器声明了组件Contact的输入输出接口,item变量用来接收来自父组件的数据源输入,update事件用于向父组件发送数据。输入输出属性分开。

定义好Contact组件输入输出接口后,接下来就与父组件ContactList交互。ContactList父组件的示例代码如下。

// import statement
@Component({
 selector: 'contact-list',
 template: `
  <!-- 使用 <contact> 标签调用Contact组件 -->
  <contact [item]="items[0]" (update)="doUpdate(newValue)"></contact>
 `
})
export class ContactListComponent {
 items: ContactModel[]
 constructor() {}
 doUpdate(item: ContactModel) {
  // ...
 }
}

ContactList组件的模板调用了Contact组件,其中[item]称为属性绑定,数据从父组件流向子组件;(update)称为事件绑定,数据从子组件流向父组件。属性绑定和数据绑定均称为数据绑定,这个是Angular强调的核心概念之一。细心的读者可能已经发现,属性绑定和数据绑定是可以直接引用组件的成员属性,如listItem和doUpdate()。属性绑定和事件绑定既用于组件数据模型和模板视图之间的数据传递,也同时用于父子组件的数据传递,在父子组件通信的过程中,模板充当类似于桥梁的角色,连接着二者的功能逻辑,如图2.13所示。

图2.13 属性绑定和事件绑定

这就是Angular的数据流动机制,然而流动并不是自发形成的,流动需要一个驱动力,这个驱动力即Angular的变化监测机制。Angular是一个响应式系统,每次数据变动几乎能实时处理,并更新对应视图。那么Angular是如何感知数据对象发生变动呢?Angular是以适当的时机去检验对象的值是否被改动,这个适当的时机并不是以固定某个频率去执行,而通常是在用户操作事件(如单击),setTimeout或XHR回调等这些异步事件触发之后。Angular捕获这些异步事件的工作是通过Zones库实现的,变化监测事件图如图2.14所示。

图2.14 变化监测事件

从图2.14可以看出,每个组件背后都维护着一个独立的变化监测器,这个变化监测器记录着所属组件的数据变更状态。由于应用是以组件树的形式组织,因此每个应用也有着对应的一棵变化监测树。当Zones捕获到某异步事件后,通常它都会通知Angular执行变化监测操作,每次变化监测操作都始于根组件,并以深度优先的原则向叶子组件遍历执行,而且每个组件的变化监测器都对其组件的数据模型经过优化,检测的性能非常高。

智能化的变化监测机制使得开发者不必关心数据何时何地被变动,它总是能找到适当的时机去触发数据检测,这就是Angular强大的数据变化监测机制特点所在。而当检测到数据发生变动时,结合数据绑定从而驱动模板视图的实时更新,这就是我们所看到的实时更新的效果。

那么我们继续延伸,倘若在发现输入数据有变动的时机里,我们需要去做一些额外的处理,怎么办?Angular提供了完善的生命周期钩子来解决这个问题,如ngOnChanges可以满足刚提到的捕获输入数据变动时机的要求,使用方法也很简单,直接定义一个同名实例方法即可。

export class ContactComponent {
 @Input() item: ContactModel; // 输入属性
 @Output() update: EventEmitter<ContactModel>; // 输出属性
 constructor() { }
 modify() {
  // ...
  this.update.emit(newValue);
 }
 ngOnChanges(changes: SimpleChanges) { // 变化检测钩子,item值变动时触发
             // changes包含了变动前后状态
  // ...
 }
}

常用的生命周期钩子如下。

1 最先触发的是构造函数,你可以做些组件类的初始化工作,例如类变量初始赋值等。

2 接下来会触发ngOnChanges钩子,这是ngOnChanges钩子的第一次触发,主要用来接收来自父组件传入的数据,为接下来的组件的初始化工作提供数据支持。

3 然后就到了ngOnInit钩子,这个才是实际意义的组件初始化阶段,Angular不推荐在构造器初始化阶段处理一些业务逻辑相关的工作,更好的方式是放在ngOnInit阶段来处理。

4 接下来,组件就进入稳定期,这个时期ngOnChanges钩子可以反复触发。只要从输入属性获取到的数据发生变化,ngOnChanges钩子就会触发。

5 最后,在组件销毁之前会触发ngOnDestroy钩子,在这个阶段可以用来做一些清理工作,如事件解绑,取消数据订阅等。

以上便是组件的简述,同时简单介绍了元数据和数据绑定,对于组件的重要要素,即模板的介绍并没有展开,Angular为模板提供了强大的功能特性。

(2)模板

Angular模板基于HTML,普通的HTML也可作为模板输入。

  @Component({
 template: `<p>张三</p>`
})

但Angular模板不止于此,Angular为模板定制出一套强大的语法体系,涉及内容颇多,这也是为什么将模板单独列出的原因。数据绑定是模板最基本的功能,除了前述提到的属性绑定和事件绑定,插值也是很常见的数据绑定语法,示例代码如下。

// import statement
@Component({
 selector: 'contact',
 template: '<p>{{ item.name }}'
})
export class ContactComponent {
 @Input() item: ContactModel;
 // ...
}

插值语法是由一对双大括号{{}}组成,插值的变量是组件类本身,如上例中的item,插值是一种单向的数据流动——从数据模型到模板视图。

上面提到的三种数据绑定(属性绑定、事件绑定以及插值)语法的数据流动都是单向的,在某些场景下需要双向的数据流动支持(如表单)。结合属性绑定和事件绑定,Angular模板可实现双向绑定的功能,如:

  <input [(ngModel)]="contact.name"></input>

[()]是实现双向绑定的语法糖,ngModel是辅助实现双向绑定的内置指令。上述代码执行后,Input控件和contact.name之间就形成双向的数据关联,Input的值发生变更时,可自动赋值至contact.name,而contact.name的值被组件类改变时,也可实时更新Input的值。

由上可知,数据绑定负责数据的传递与展示,而针对数据的格式化显示,Angular提供了一种叫管道的功能,使用竖线|来表示,示例代码如下。

  <span>{{contact.telephone|phone}}</span>

假设上述contact.telephone的值是18612345678,这一串数字并不太直观,管道命令phone可以将其进行美化输出,如“186-1234-5678”,而不影响contact.telephone本身的值。管道支持开发者定制开发,phone即属于自定义管道。Angular也提供了一些基本的内置管道命令,如格式化数字的number、格式化日期的date等。

上述是Angular模板主要的语法特性,除了基本的语法特性,模板还有一套强大的“指令”机制,来简化一些特定的交互场景,如样式处理、数据遍历以及表单处理等。

(3)指令

指令与模板关系密切,可以与DOM进行灵活交互,它或是改变样式,或是改变布局。了解过AngularJS的开发者可能会有疑问,这里的指令跟AngularJS的指令是一回事吗?虽然Angular指令跟AngularJS指令在功能有类似之处,但二者并不完全是同一个概念。Angular指令的范畴很广,实际上组件也是指令的一种。组件与一般指令的区别在于:组件带有单独的模板,即DOM元素,而一般的指令是作用在已有的DOM元素上。一般的指令分为两种:结构指令和属性指令。

结构指令能够添加、修改或删除DOM,从而改变布局,如ngIf。

  <button *ngIf="canEdit"> 编辑…</button>

当canEdit的值为true时,button按钮会显示到视图上;当canEdit为false时,button按钮会从DOM树上移除。注意结构指令的*号不能丢掉,这是Angular为了使用简便实现的语法糖。

属性指令用来改变元素的外观或行为,使用起来跟普通的HTML元素属性非常相似,如ngStyle指令,用于动态计算样式值,示例代码如下。

  <span [ngStyle]="setStyles()">{{contact.name}}</span>

标签的样式由setStyles()函数计算得出,setStyles()是其组件类的成员函数,返回一个计算好的样式对象,示例代码如下。

class ContactComponent {
 private isImportant: boolean;
 setStyles() {
  return {
   'font-size': '14px',
   'font-weight': this.isImportant ? 'bold' : 'normal'
  }
 }
}

上面列举的ngIf和ngStyle都是Angular的内置指令,类似的还有ngFor、ngClass等,这些内置指令为模板提供了强大语法支持。指令更具吸引力的地方在于支持开发者自定义,自定义指令能最大限度地实现UI层面的逻辑复用。

(4)服务

服务是封装单一功能的单元,类似于工具库,常被引用于组件内部,作为组件的功能扩展。那服务包含什么?它可以是一个简单的字符串或是JSON数据,也可以是一个函数甚至是一个类,几乎所有的对象都可以封装成服务。以日志服务为例,一个简单的日志服务如下所示。

// import statement
@Injectable()
export class LoggerService {
 private level: string;
 setLevel(level: string) { 
  this.level = level;
 }
 debug(msg: string) { }
 warn(msg: string) { }
 error(msg: string) { }
}

@Injectable()是服务类装饰器。

这个服务的功能很简单,只专注于日志功能,Angular应用里每个组件都可以复用到这个日志服务,使自己有新增日志记录的能力,而不需要每个组件重复实现,这就是设计服务的主要原则。那么服务怎么样为组件所使用?这就需要引入依赖注入机制。

(5)依赖注入

在服务小节里提到过“注入”这个概念,依赖注入一直都是Angular的卖点。通过依赖注入机制,服务等模块可以被引入到任何一个组件(模块,或其他服务)中,而开发者无须关心这些模块是如何被初始化。因为Angular已经帮你处理好,包括该模块本身依赖的其他模块也会被初始化。图2.15所示为当组件注入日志服务后,日志服务以及它所依赖的基础服务都会被初始化。

图2.15 Angular的依赖注入

可以说,依赖注入是一种帮助开发者管理模块依赖的设计模式。在Angular中,依赖注入与TypeScript相结合提供了更好的开发体验。在TypeScript中,对象通常被明确赋以类型,通过类型匹配,组件类便可知道该用哪种类型实例去赋值变量。一个简单的依赖注入例子如下所示。

import {LoggerService} from './logger-service';
// other import statement
@Component({
 selector: 'contact',
 template: '...'
 providers: [LoggerService]
})
export class ContactListComponent {
 constructor(logger: LoggerService) { 
  logger.debug('xxx');
 }
}

@Component装饰器中的providers元数据是依赖注入操作的关键,它会为该组件创建一个注入器对象,并新建LoggerService实例存储到这个注入器中。当组件需要引入LoggerService实例时,只需在构造函数中声明LoggerService类型的参数,Angular自动地通过类型匹配,找出注入器里预先实例化好的LoggerService对象,在组件实例化时作为参数传入,这样组件便获得了LoggerService的实例引用。

值得注意的是,组件上创建的这个注入器对象是可以被子组件复用的,这就意味着我们只需在根组件上注入一次服务,即在根组件的providers声明注入该服务,整棵组件树上的组件都能使用这个服务,并且保持单例。这个特性非常有用,大大节省了服务的内存占用,并且由于服务是单例的,注入到组件后,可以作为中转桥梁,实现这些组件之间的数据传递。

此时,大家可能会有疑问,在某个组件分支里,我不想继续沿用这个实例了,我希望使用一个新实例,这种场景其实并不少见,那么这种情况Angular怎么解决?答案是分层注入。组件树上的每个组件都能单独注入服务,服务的每一次注入(也就是使用providers声明),该服务都会被创建出新的实例,该组件及其所有子组件都会转而使用这个新的实例,举例如下。

我在根组件注入了LoggerService日志服务,并设置日志级别level为warn级别。

// import statement
// 根组件
@Component({
 selector: 'app',
 template: '...',
 providers: [LoggerService]
})
class…AppComponent {
 constructor(logger: LoggerService) {
  logger.setLevel('warn');
 }
}

那么组件树上的所有组件都能使用到这个warn级别的日志服务。

// ContactList组件
@Component({
 selector: 'contact-list',
 template: '...'
})
class ContactListComponent {
 constructor(logger: LoggerService) { // 在构造器里声明即可
 }
}

接下来,随着业务发展,我希望在ContactList组件分支上能输出更高级别(如debug级别)的日志信息,很显然,通过在ContactList组件里修改level会有一些副作用。

class ContactListComponent {
 constructor(logger: LoggerService) {
  logger.setLevel('debug'); // 这个logger是全局实例
 }
}

它获取到的logger是根组件注入的实例,在任何一个子组件调用setLevel()都是全局生效的,这使得根组件也输出了debug级别的信息。这时候我们只需要在ContactList组件里重新注入LoggerService实例,即可满足需求。

// ContactList组件
@Component({
 selector: 'contact-list',
 template: '...',
 providers: [LoggerService] // 重新注入
})
class ContactListComponent {
 constructor(logger: LoggerService) {
  logger.setLevel('debug');
 }
}

ContactList分支使用的是新的debug级别的日志服务,而根组件和Header等其他组件依然能继续使用warn级别的日志服务,如图2.16所示。

图2.16 注入树

组件以树的形式组织,使得组件背后的注入器对象也可以抽象为一棵树,称为注入树。Angular首先会从宿主组件对应的注入器查找匹配的服务实例,若找不到,则继续往父组件的注入器里查找,一直找到最顶层的注入器为止,若都找不到匹配的实例,则抛出错误。这种灵活的注入方法可以适应多变的应用情景,既可配置全局单例服务(在应用的根组件注入即可),也可按需注入不同层级的服务,彼此数据状态不会相互影响。

前面提到过,依赖注入除了可以作用于组件,也可以作用于模块,要理解模块的依赖注入,首先理解模块是什么。

(6)模块

首先说明的一点,模块有两层含义:

1 框架代码以模块形式组织(物理模块);

2 功能单元以模块形式组织(逻辑模块)。

物理模块是TS/ES6提供的文件模块特性,并不是本书重点,这里重点剖析的是逻辑模块,下面将逻辑模块简称为模块。

一个大型应用由大量组件、指令、管道、服务构成,这些构件中有些是没有交集的,而有些则协同工作来完成某个特定的功能,我们希望把这些有关联的构件包装到一块,形成一个比较独立的单元,这样的单元在实际意义上就称为模块。所以简单说,模块就是对应用零散的组件、指令、服务按功能进行归类包装。其关系示意图如图2.17所示。

图2.17 Angular模块的概念

除此之外,模块还有一个重要的实际意义。因为默认情况下,一个组件是不能直接引用其他组件的,也不能直接使用其他指令的功能,要想使用需要先导入。前面讲父子组件时候已经提到过,这个导入的过程就是应用模块实现的。总结来说,一个组件可以任意使用同模块的其他组件和指令。但是,跨模块里的组件指令则不能直接相互使用,如模块A的组件不能直接使用模块C的指令,若要跨模块访问,则需结合模块的导入导出功能,要理解导入导出的内容,先来看一个简单的模块例子。

  // import statement
  @NgModule({
   imports: [SomeModule], // 导入其他模块
   declarations: [SomeComponent, SomeDirective, SomePipe], // 引入组件、指令、管道
   providers: [LoggerService], // 依赖注入
   exports: [SomeComponent, SomeDirective, SomePipe] // 导出组件、指令、管道
   // bootstrap: [AppComponent] // 根模块才有,标记哪个组件是根组件
  })
  export class AppModule { }

可以看出,声明模块使用的是@NgModule()装饰器。先来看imports和exports属性,它们为模块的导入导出属性,模块间的导入导出关系如图2.18所示。

图2.18 模块间的导入导出关系

由图2.18可知,模块A导入了模块B,模块B通过exports属性暴露了组件B1和指令B2。很显然,组件B1和指令B2能够被组件A1使用,而组件B3并不能。所以可以看出,Angular模块既可以对外暴露出一些构件,同时又有一定的封装性,能够隐藏内部的一些实现。

讲完了模块内的组件和指令(管道的访问方式跟组件指令一致),接下我们来看一下服务。服务既可注入到组件也可注入到模块,二者的使用方法大致相同,区别在于作用域。所有的模块上都共享着一个应用级别的注入器,这就意味着注入到任何一个模块的服务可以在应用全局(所有模块)里使用,而注入到组件里的,仅能在该组件以及它的子组件上使用。

可以看出,组件级注入器是全局注入器的一个子注入器,所以回看上面这个例子,模块B里的服务B4既可以在模块A里使用,也可以在模块C里使用。

这里大家可能会有疑问,如果不同的模块里都注入了相同标识的服务,由于模块都共享同一个注入器,免不了会发生冲突。这里,只要记住一个原则即可,后初始化的服务会覆盖先初始化的服务,举个例子,模块A和模块C都注入了LoggerService,并且模块A导入了模块C,由于模块C会先初始化,然后才到模块A,所以模块A注入的LoggerService会被应用到全局。特别提醒的一点是,即使模块C也注入了LoggerService,该模块里生效的实例也会是模块A里注入的那个实例,一定要记住这点。按照这个理论来推导,根模块里注入的服务始终是最高优先级的。

上面主要介绍了模块的特性,接下来看一下Angular给我们推荐的模块使用的最佳实践。

1 首先,Angular要能成功运行,至少需要定义一个模块,因为需要有一个模块作为应用启动的入口,这样的模块就称为根模块。

2 其次,我们的应用会不断地添加新的功能,这些新增的功能可以封装到一个新的模块里。这些新增加的模块在Angular里称为特性模块。有了特性模块之后,根模块原来承载的功能逻辑也可以抽离出来,放到某个特性模块里,使根模块保持简洁。

3 再次,我们添加的特性模块越来越多,它们之间可以抽出一些相似功能的组件或指令,这些公共的部分也可以封装成一个独立的模块,这样的模块在逻辑意义上不能称为特性模块,Angular把它称为共享模块。

4 最后,还有核心模块,我们知道,一个应用里总有一些全局的组件或服务等,它们只需要在应用启动时候初始化一次即可,例如,维护登录信息的服务,或者是,公共的头部和尾部组件等。虽然我们可以把它们放到根模块里,但更好的设计是把这些逻辑也抽离出来,放到一个独立的模块中,这个模块即为核心模块。核心模块要求只导入到根模块里,尽量不要导入到特性模块或者共享模块里,这是为了在协同工作时候避免出现一些不可预料的结果。

这就是Angular给我们推荐的最佳实践。最终我们看到,处于总指挥地位的根模块非常简洁,没有烦琐的业务细节。应用的功能特性被切分为各个大大小小的模块,逻辑结构非常清晰。Angular已经封装了不少常用的模块,如下所示。

1 ApplicationModule:封装一些启动相关的工具;

2 CommonModule:封装一些常用的内置指令和内置管道等;

3 BrowserModule:封装在浏览器平台运行时的一些工具库,同时将Common Module和ApplicationModule打包导出,所以通常在使用时引入BrowserModule就可以了;

4 FormsModule和ReactiveFormsModule:封装表单相关的组件指令等;

5 RouterModule:封装路由相关的组件指令等;

6 HttpModule:封装网络请求相关的服务等。

所以,如果你想使用ngIf和ngStyle等这些内置指令,记得先导入CommonModule,其他的模块使用方法参照执行。

(7)应用启动

Angular通过引导运行根模块来启动应用,引导的方式有两种:动态引导和静态引导。要理解二者的区别,先来简述Angular应用的启动过程,Angular应用运行之前,都需要经过编译器对模块、组件等进行编译,编译完后才开始启动应用并渲染界面。

动态引导和静态引导的区别就在于编译的时机不同,动态引导是将所有代码加载到浏览器后,在浏览器进行编译;而静态引导是将编译过程前置到开发时的工程打包阶段,加载到浏览器的将是编译后的代码。假设我们的根模块为AppModule,动态引导的示例代码如下。

  import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
  import { AppModule } from './app.module';
  platformBrowserDynamic().bootstrapModule(AppModule);

动态引导是从platformBrowserDynamic函数启动,该函数从@angular/platform-browser-dynamic文件模块中导入。动态引导启动的模块AppModule即我们编写的模块,再来看看静态引导的示例代码。

  import { platformBrowser } from '@angular/platform-browser';
  import { AppModuleNgFactory } from './app.module.ngfactory';
  platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);

静态引导以platformBrowser函数启动,这个函数是从@angular/platform-browser文件模块中导入的,跟动态引导文件不是同一个。静态引导启动的是AppModuleNgFactory模块,这是AppModule经过编译处理后生成的模块(app.module文件编译后生成app. module.ngfactory文件)。由于整个应用已经被预先编译,所以编译器并不会打包到项目代码,代码包体更小,加载更快,而且也省去了浏览器编译这个步骤,因此应用启动的速度也会更快。

动态引导开发流程简单明了,适合小型项目或者大型应用的开发阶段使用;而静态引导需要在开发阶段加入预编译流程,稍显复杂但性能提升明显,任何时候都推荐使用。

3. JavaScript也在进化中——理解ES6和TypeScript

近几年来,Web开发者突然发现,有许多意图创建出更好的或者更能满足开发者需要的JavaScript版本出现了。CoffeeScript、AtScript、Dart、ES6、TypeScript等,都试图改进JavaScript。这些语言通过提供符合现代App开发的特性和功能来扩展JavaScript。但每一种语言都必须解决这个问题,即我们现在的浏览器所使用的JavaScript版本是所谓的ECMAScript5 (ES 5),这样每一种语言都必须将它的代码转换成标准的JavaScript,即所谓的转译。

如果你想使用下一代的JavaScript,通常有两种选择:ES6或者TypeScript。ES6是JavaScript的下一代官方版本,它在2015年1月正式通过,随着时间的推移,可能会逐渐成为浏览器支持的正式标准,它提供了强大的类型检查和面向对象特性。它的内核也使用了ES6。TypeScript是Angular和Ionic App开发中的主要语言。

尽管我们当前的浏览器不支持这两种语言,但可以用一些工具例如Babel或tsc将我们的代码转译成标准JavaScript。我们不需要关心这个过程,因为它已经内置在Ionic编译过程中了。

TypeScript扩展了JavaScript语法,任何已经存在的JavaScript程序,可以不加任何改动,在TypeScript环境下运行。TypeScript只是向JavaScript添加了一些新的遵循ES6规范的语法,以及基于类的面向对象编程的这种特性。本节介绍的Angular框架,这个框架本身是由TypeScript编写的,而Angular框架是由谷歌公司开发的。也就是说,现在TS这门语言是由微软和谷歌这两大公司在背后支持。因此我们有理由相信,在未来一段时间内,TS有可能成为前端开发语言中的主流。

(1)搭建TypeScript开发环境

其实也就是安装TypeScript的转译器。

1 安装。

  $ npm install -g typescript

2 版本检查。

  $ tsc --version
                           Version 2.3.4

3 简单使用。

在work文件夹下新建Hello.ts文件。

export class Hello {
  
}

运行$tsc Hello.ts命令,可以看到在work文件夹下生成了一个hello.js文件,即编译后最终在浏览器中执行的文件。

"use strict";
exports.__esModule = true;
var Hello = (function () {
  function Hello() {
  }
  return Hello;
}());
exports.Hello = Hello;

(2)字符串新特性

1 多行字符串:在传统的JS中,当你想给一个字符串换行的时候需要用"+";在新的TS中,引用了多行字符串的概念。使用双撇号声明字符串。

var content = `aaa
bbb
ccc`;

转译成JS会自动加上换行。

var content = "aaa\nbbb\nccc";

2 字符串模板:Angular的一个特点是它内置了模板引擎。很多时候,模板是以外部文件的方式存在的。但有时候,将模板直接写在代码行中可能更直观。模板字符串不一定是静态的,你可以通过$(表达式)的方式进行字符串插值。

var myname = "qiang lei";
var getName = function(){
  return "qiang lei";
}
console.log(`<div>
<span>${myname}</span>
<span>${getName()}</span>
<div>xxx</div>
</div>`);

(3)变量、参数和类型新特性

1 在ES6中,可以用let关键字指定一个变量,使变量的作用域仅限于最近的块。

2 参数类型:在参数名称后面使用冒号来指定参数类型,可以避免错误类型复制,减少开发中的错误,在TS转译器中会提示错误!此外,函数返回值也可以类型化。

3 参数默认值:在参数声明后面用等号来指定参数默认值。

4 可选参数:在方法的参数声明后面用问号来标明此参数为可选参数。

5 类型化数组:在有效的类型声明之后添加[]后缀,例如:boolean[]。

6 特殊类型:除了原始类型外,在TypeScript中还有几个类型有着特殊的含义。它们是any(可以保存任意类型);nul和lundefined(可以看成是一种any类型);void。

(4)函数新特性

1 Rest and Spread操作符:用来声明任意数量的方法参数,操作符为三个点:“...”。

function fun1(...args){
  args.forEach(function(arg){
    console.log(arg);
  })
}
fun1(1, 2, 3);
fun1(7, 8, 9, 10, 11, 12);
// "..."即Rest and Spread操作符

2 generator函数:控制函数的执行过程,手工暂停和恢复代码执行。generator函数和普通函数的写法有区别,一是function关键字和函数名之间有一个星号,二是函数体内部使用yield语句,定义不同的内部状态。

function* getStockPrice(stock){
  while(true){
    yield Math.random()*100;
  }
}
var priceGenerator = getStockPrice("QL");
var limitPrice = 14;
var price = 100;
while(price > limitPrice){
  price = priceGenerator.next().value;
  console.log(`the gernerator return ${price}`);
}
console.log(`buying at ${price}`);

3 Promise对象:用于延迟操作和异步操作。一个Promise表示一种未完成的操作,这个操作有可能在未来完成。当需要和远程服务器打交道或者加载本地数据时,Prromis非常有用,它提供了一种比传统回调方法更简单的方式来处理异步操作。一个Promise可能有三种状态。Pending:结果还不确定,因为异步操作还未返回结果;Fulfilled:异步操作已经完成,Promise已经有确定值;Rejected:异步操作失败,Promise永远不会出现。Promise的主要API是then方法,这个方法注册了一个回调方法用于接收结果值或者失败原因。

4 Observable对象:Angular有许多服务使用Observable而不使用Promise。Observable是用RxJS库来实现。与Promise用于解决单个值的同步不同,一个Observable用于解决多个值的同步(实时)。

(5)表达式和循环

1 箭头表达式(lambda表达式):lambda表达式()=>{something}或()=>something相当于js中的函数,它的好处是可以自动将函数中的this附加到上下文中,用来声明匿名函数,消除传统匿名函数的this指针问题,如下说明。

var sum = (arg1, arg2) => {
  return arg1 + arg2;
}

2 forEach()遍历数组, for in(ES5)遍历的是数组的索引(键名),for of(ES6)遍历的是数组元素值,用下面的例子予以说明。

var myArray = [1,2,3,4];
myArray.forEach(value => console.log(value));
for(var n in myArray){
  console.log(n); //0 1 2 3 4 desc属性名(键名)
  console.log([n]); // 1 2 3 4 four number键值
}
for(var n of "four number"){
  console.log(n); // four number;把每个字符打印出来
}

(6)面向对象特性

TypeScript类(Class):类是TS的核心,使用TS开发时,大部分代码都是写在类里面的。这里会介绍类的定义、构造函数以及类的继承。

1 定义。

class Person{
  constructor(public name:string){
    this.name = name;
    console.log("haha");
  }// 类的构造函数,只在实例化时被调用,而且被调用一次;
  eat(){
    console.log("im eating");
  }
}
class Employee extends Person{
  code: string;
  constructor(name: string, code: string){
    super(name);//子类构造函数必选调用父类构造函数
    console.log("xixi");
    this.code = code;
  }
  work(){
    super.eat();//调用父类方法
    this.doWork();
  }
  private doWork(){
    console.log("im working");
  }
}
//类的实例化
var e1 = new Employee("name", "1");
e1.work();
var p1 = new Person("batman");
p1.eat();
var p2 = new Person("superman");
p2.eat();

2 TypeScript泛型(generic):参数化的类型,一般用来限制集合的内容;继续上面的例子,声明一个数组。

var workers: Array<Employee> = [];//指定数组只能放Person类型的元素
workers[0] = new Employee("zhangsan","1");
workers[1] = new Employee("lisi", "2");
workers[0].work();
workers[1].work();

3 TypeScript接口(Interface):用来建立某种代码约定,使得其他开发者在调用某个方法或创建新的类时必须遵循接口所定义的代码约定。示例如下。

//声明接口
interface IPerson{
  name: string;
  age: number;
  
} 
//声明类
class Person{
  //接口作为方法的参数的类型声明
  constructor(public config: IPerson){
  }
}
var p1 = new Person({
  name: "zhangsan",
  age: 18
})//传入的参数必须满足接口声明的参数的所有属性

4 TypeScript模块(Module)

模块可以帮助开发者将代码分割为可重用的单元。开发者可以自己决定将模块中的哪些资源(类、方法、变量)暴露出去供外部使用,哪些资源只在模块内使用。在TS中一个文件就是一个模块,模块的内部有两个关键字(export、import)支撑模块的特性,用这两个关键字来控制模块对外暴露什么以及需要别的模块为你提供什么。

5 TypeScript注解(Annotation):注解为程序的元素(类、方法、变量)加上更直观的说明,这些说明信息与程序的业务逻辑无关。例如Angular中的组件@Component就是个注解,这个注解本身是由Angular框架提供的,在这个注解中有一些属性,这些属性会告诉Angulr2框架怎么去处理AppComponent这个ts类,即在Angular2框架中,去实例化一个AppComponent这样一个类的时候,Angular框架会去加载./app.component.html这个页面和./component.css展示的页面。这就是注解,它用来告诉我们一个框架怎么处理程序中的类、元素等。

6 TypeScript类型定义文件(*.d.ts):类型定义文件用来帮助开发者在TypeScript中使用已有的JavaScript的工具包。

2.4.5 Apache Cordova(PhoneGap)

(1)概述

Ionic框架构建于另外两种技术之上:Angular和Apache Cordova。上一节我们已经介绍了Angular,在本节我们将学习Apache Cordova是什么以及它如何与Ionic交互。

Apache Cordova是一个开源框架,允许移动App开发者用HTML、CSS和JavaScript内容创建针对各种移动设备的本地应用。Cordova会将你的Web应用渲染到原生Web View中,Web View是一个原生App组件,该组件用于在原生App中显示Web内容。你可以把Web View看成是没有任何标准用户界面外壳的Web浏览器,如图2.19所示。Web App可以在这个容器中运行,就像任何其他类型的Web应用在移动浏览器中运行一样,它可以打开外部HTML页、执行JavaScript代码、播放媒体文件、与远程服务器交互。这种移动App类型通常被称作混合App。

图2.19 Cordova架构

此外,Cordova还提供了允许访问各种设备特性(如联系人数据库)的JavaScript API。这些能力通过一系列插件暴露给开发者。插件提供了一个Web App和设备原生能力之间的桥接层。Cordova项目维护了一个核心的插件集合,但更多功能是通过第三方插件来提供的(如NFC通信、压感触控)。

Cordova的优势如下。

1 Cordova为构建混合移动应用程序提供了一个平台,因此我们可以开发一个应用程序,在不同的移动平台,如iOS、Android、Windows Phone、Amazon-fireos、黑莓、Firefox OS、Ubuntu上使用。

2 开发混合应用程序比原生应用程序更快,所以Cordova可以节省大量的开发时间。

3 由于我们在使用Cordova时使用JavaScript,因此不需要学习平台特定的编程语言。

4 有大量的社区插件可以与Cordova一起使用,许多库和框架都经过优化。

(2)深入了解Cordova

Cordova CLI的出现使Cordova的配置变得简单了。CLI会搭建一个基本的项目脚手架并将它配置成为支持你指定的所有移动平台。Cordova CLI还允许我们轻松地集成和管理项目中的插件。

每个Cordova App都是通过config.html文件来配置的。这个全局的配置文件控制了App的方方面面,从App的图标、插件到和平台相关的各种设置。它是基于W3C的Package Web App(Widgets)规范的。Ionic CLI在搭建脚手架的过程中会生成一个基本的config.xml文件,如图2.20所示。

图2.20 Cordova的config.xml配置文件

config.xml的主要元素如下。

1 widget:创建应用程序时指定的应用程序反向域值。

2 name:创建应用程序时指定的应用程序名称。

3 description:应用程序说明。

4 Author:应用程序的作者。

5 content:应用程序的起始页,它位于www目录内。

6 plugin:当前安装的插件。

7 access:用于控制对外部域的访问。默认的origin值设置为*,这意味着允许访问任何域。此值不允许打开某些特定的网址来保护信息。

8 allow-intent:用于控制对外部域的访问。默认的origin值设置为*,这意味着允许访问任何域。此值不允许打开某些特定的网址来保护信息。

9 platform:构建应用程序的平台。

Cordova的真正强大之处是它的插件库。什么是Cordova插件?这里给出了Cordova官方网站上的定义:一个插件是一些任务的代码,允许JavaScript和原生组件交互。它们允许你的App能够调用单纯的Web App所不具备的原生设备的能力。

在前面我们提到过,有两套插件:核心插件和第三方插件。从PhoneGap 3.0开始,所有的插件都抽离成单独的元素。这种改变意味着所有的插件都可以按需升级,而无须更新整个代码库。

虽然Cordova为开发者提供了大量的功能,例如跨平台、对Web进行扩展、调用设备原生功能,但它仍然缺少一个重要的组件:用户界面组件。默认只提供基本的HTML控件,不提供任何可以在原生SDK,如Android中找到的任何组件。如果你想拥有一个tab bar组件,你要买编写HTML标签、CSS样式及其负责管理这些视图的JavaScript,要么就使用第三方框架,如Ionic。

此外,Cordova所用的Web View到底来自哪里?当你编译App时,Cordova并不会在你的App中放进一个Web View,它其实使用的是设备内置的Web View。因此,如果要在旧的移动设备上开发App的话,需要将设备自带的Web View替换成最新的Web View,如谷歌的Chromium,从而获得最新的浏览器特性。尽管这个方法会明显增加App的大小,却有助于规范你能够调用的Web特性。

2.4.6 理解Ionic

1. Ionic页面的构成

每个Ionic页面(App中的一个组件,如一个底部tab bar)由三种基本文件组成:一个HTML文件,用于定义需要显示的组件;一个SAAS文件,用于定义这些组件的可视化样式;一个TypeScript文件,用于提供这些组件的自定义功能。

(1)HTML文件构成

Ionic的一个组件页面和传统的HTML文件不同,不需要使用<head><body>等标签,这个HTML文件在index.html中定义。我们只需要定义真正需要展示给用户的组件。这些组件混合了传统HTML标签和自定义标签,这些自定义标签用于定义Ionic组件。

Ionic框架利用Angular的自定义标签扩展HTML标签的能力创建了一整套移动组件,这些组件包括<ion-card><ion-item-sliding><ion-segment-button>。与标准的HTML标签一样,Ionic组件也可以接受各种属性来定义它们的设置,例如设置它们的ID值或者定义附加的CSS类,当然Ionic组件也有一些专门的属性。

(2)SASS/SCSS文件

Ionic App的界面用CSS进行定义。但是这个CSS在Ionic中是用SASS(SyntacticallyAwesome Style Sheets,CSS预处理器)来创建的。SASS比CSS有几个好处,其中就包括了变量声明,例如$company-brand:#ff11dd。然后就是引用这个变量而不用直接使用颜色值。所有的Ionic组件都可使用SASS变量来设置样式。

另外SASS支持一种改良语法,用于编写嵌套的CSS。用这种方式写CSS更加简洁,更节省时间。通常,ionic组件页面的.scss文件用于定义和某个页面相关的CSS。对于那些App级别的主题样式,则可以在app.core.scss文件中进行样式的定义。

(3)理解TypeSacript

编写Ionic页面的最后一个元素是相关的TypeScript(.ts)文件。这个文件用于编写和页面交互逻辑相关的Anguar/TypeScript代码。这个文件会定义所有需要导入到页面中的代码模块,一般包括那些需要在代码中用到的组件(例如导航到新的页面)或者负责提供必要功能的Angular模块(例如发送一个HTTP请求)。

2. Ionic框架的资源优势

Ionic基于Angular基础框架开发并集成Cordova发展而来,Angular负责界面交互,业务逻辑处理等;Cordova则搭建起Javascript与设备功能的桥梁,使得通过调用相应的API即能使用硬件设备功能,如摄像头,麦克风等。此外ionic还开发了大量的适合在移动设备上使用的界面控件(通过标签指令使用)以及Ionic核心工具CLI(Command Line Interface)。

1 Ionic预置了大量的资源,包括图标、控件、特定样式等;数百个内置图标可以拿来即用。

  <!-- Infomation Tab -->
<ion-tab title="资讯" icon-off="ion-ios-information" icon-on="ion-ios-information" ui-sref="tab.infos.list1">
<ion-nav-view name="tab-infos"></ion-nav-view>
</ion-tab>

2 针对移动设备特征优化过的内置控件,如导航、内容块、滚动、侧栏菜单、键盘等,包括Action Sheet、Backdrop、Content、Form Inputs、Gestures and Events、Headers/Footers、Keyboard、Lists、Loading、Modal、Navigation、Platform、Popover、Popup、Scroll、Side Menus、Slide Box、Spinner、Tabs、Tap&Click。

例子:Lists。

<ion-list>
<ion-item ng-repeat="item in items">
Hello, {{item}}!
</ion-item>
</ion-list>

3 针对表格、按钮、列表和一般布局的内置CSS模块,可以产生层次分明的布局效果和类似手机原生应用的视觉感,如头部、内容、脚部、列表、单选、多选、按钮等内置样式等,包括Header、Sub Header、Content、Footer、Buttons、List、Cards、Forms、Toggle、Checkbox、Radio Buttons、Range、Select、Tabs、Grid。

例子:Header。

<div class="bar bar-header bar-light">
<h1 class="title">bar-light
</div>

这些基础控件以及样式的内置使开发人员可以从烦琐、耗时的布局工作中解放出来(只需使用简单的控件标签指令配合特定效果的内置CSS样式,即可产生漂亮的显示效果)。

4 沿用了Angular的优秀设计,通过框架复用,解放程序员。

业务逻辑处理、数据动态绑定,个性化视图展示等也沿用了Angular的优秀设计,通过框架复用,可以大大减少开发的代码量,只需要专注于业务逻辑的处理以及数据的分流格式化,可以通过数个模块(js文件)配合得以来完成。

5 Ionic配套工具CLI也使得创建,转化为适合具体平台运行的App(Android、iOS等),安装调试等变得非常简便。

3. Ionic项目剖析

本部分将对一个真实Ionic项目的结构进行剖析。

(1)第一次产生Ionic2应用程序,即生成的项目结构

1 config.xml。

2 hooks。

3 ionic.config.json。

4 node_modules。

5 package.json。

6 platforms。

7 plugins。

8 resources。

9 src。

10 tsconfig.json。

11 tslint.json。

我们几乎总是会花90%的时间在src文件夹,这就是应用程序逻辑。然而整个应用程序通常从一个简单的src/index.html文件开始。

(2)src/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="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('assets/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>

src/index.html是项目的起始入口。在这个文件内会有<ion-app></ion-app>标签。ionic查找这个标签来运行App,<script src="cordova.js"></script>本地部署时会将Cordova打包到项目中,让我们可以使用Cordova创建应用(将应用打包为native应用,可以提交到App Store);<script src="build/main.js"></script>代码编译打包的最终文件,把ionic、Angular和我们的App连接起来。

(3)简要描述项目结构

1 config.xml:这包含配置应用程序的名称和包名,被用于将我们的应用程序安装到一个实际的设备。

2 src:这就是我们花费大部分的时间建立的应用程序,它包含结构化程序的源代码。

3 node_modules:包含了npm包。

4 package.json:这些都是包构建ionic应用程序所必需的内容。

5 platforms:这就是平台的具体构建,构建工具和包/库存储。

6 plugins:这就是Cordova plugins。Cordova插件允许你的应用调用移动设备本地功能,如访问媒体存储设备,甚至蓝牙API。

7 resources:这也包含特定于平台的资源(如图标和启动屏幕)。

(4)src/app/app.module.ts

src/app/app.module.ts是App的入口点。

import { NgModule } from '@angular/core';
import { IonicApp, IonicModule } from 'ionic-angular';
import { MyApp } from './app.component';
import { HelloIonicPage } from '../pages/hello-ionic/hello-ionic';
import { ItemDetailsPage } from '../pages/item-details/item-details';
import { ListPage } from '../pages/list/list';
……
@NgModule({
 declarations: [
  MyApp,
  HelloIonicPage,
  ItemDetailsPage,
  ListPage
 ],
 imports: [
  IonicModule.forRoot(MyApp)
 ],
 bootstrap: [IonicApp],
 entryComponents: [
  MyApp,
  HelloIonicPage,
  ItemDetailsPage,
  ListPage
 ],
 providers: []
})
export class AppModule {}

每个App都有一个根模块控制应用,这很像Ionic和Angular1的ng-app,同样也是使用ionicBootstrap引导App的地方。这个模块中,设置根组件到src/app/app.component. ts里面的MyApp。这是App加载的第一个组件,通常是用于其他组件加载的空壳。在app. component.ts中,设置了src/app/app.html的模板。

(5)src/app/app.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 id="nav" [root]="rootPage" #content swipeBackEnabled="false"></ion-nav>

在这个模板中,我们建立ion-menu作为一个菜单,建立一个ion-nav组件作为主要内容区域。ion-menu的[content]属性绑定到本地变量ion-nav中的content,所以可知道它的动作。我们不会碰这个文件,仅使用它生成的,操作按钮执行一个*ngFor=“让p成为页面”的指令,这就是Angular执行重复的模板。这仅仅意味着遍历页面收集和生成一个集合中的每一项的模板,与此html文件对应的ts文件是src/app/app.component.ts。

(6)src/app/app.component.ts

export class MyApp {
 @ViewChild(Nav) nav: Nav;
 // make UsersPage the root (or first) page
 rootPage: any = UsersPage;
 pages: Array<{title: string, component: any}>;
 constructor(public platform: Platform, public menu: MenuController) {
  this.initializeApp();
  // set our app's pages<br>
  this.pages = [
   { title: 'Users', component: UsersPage },
   { title: 'Repos', component: ReposPage },
   { title: 'Organisations', component: OrganisationsPage },  
  ];
 }
 
 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);
 }
}