Ionic-学习手册第二版-全-

Ionic 学习手册第二版(全)

原文:zh.annas-archive.org/md5/2E3063722C921BA19E4DD3FA58AA6A60

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

本书解释了如何使用 Ionic 轻松构建混合移动应用。无论是与 REST API 集成的简单应用,还是涉及原生功能的复杂应用,Ionic 都提供了简单的 API 来处理它们。

凭借对网页开发和 TypeScript 的基本知识,以及对 Angular 的良好了解,一个人可以轻松地将百万美元的创意转化为一款只需几行代码的应用。

在本书中,我们将探讨如何实现这一点。

本书涵盖的内容

第一章,Angular - 入门,向您介绍全新 Angular 的强大功能。我们将了解 TypeScript 的基础知识和理解 Angular 所需的概念。我们将学习 Angular 模块、组件和服务,并通过构建一个应用来结束本章。

第二章,欢迎使用 Ionic,介绍了名为 Cordova 的混合移动框架。它展示了 Ionic 如何融入混合移动应用开发的大局。本章还介绍了使用 Ionic 进行应用开发所需的软件。

第三章,Ionic 组件和导航,带您了解 Ionic 的各种组件,从页眉到导航栏。我们还将学习使用 Ionic Framework 在页面之间进行导航。

第四章,Ionic 装饰器和服务,探讨了我们用于初始化各种 ES6 类的装饰器。我们还将学习平台服务、配置服务以及其他一些内容,以更好地理解 Ionic。

第五章,Ionic 和 SCSS,讨论了如何利用内置的 SCSS 支持为 Ionic 应用设置主题。

第六章,Ionic Native,展示了 Ionic 应用如何使用 Ionic Native 与设备功能如相机和电池进行接口交互。

第七章,构建 Riderr 应用,展示了本书如何构建一个端到端的应用,该应用可以使用本书迄今为止所学的知识与设备 API 和 REST API 进行接口交互。我们将构建的应用将是 Uber API 的前端。使用这个应用,用户可以预订 Uber 车辆。

第八章,Ionic 2 迁移指南,展示了如何将使用 Ionic Framework v1 构建的 Ionic 应用迁移到 Ionic 2,并且相同的方法也适用于 Ionic 3。

第九章,测试 Ionic 2 应用,将带您了解测试 Ionic 应用的各种方法。我们将学习单元测试、端到端测试、monkey 测试以及使用 AWS Device Farm 进行设备测试。

第十章,发布 Ionic 应用,展示了如何使用 Ionic CLI 和 PhoneGap Build 生成 Cordova 和 Ionic 构建的应用的安装程序。

第十一章,Ionic 3,讨论了升级到 Angular 4 和 Ionic 3 的内容。我们还将了解 Ionic 3 的一些新功能。

附录,展示了如何有效地使用 Ionic CLI 和 Ionic 云服务来构建、部署和管理您的 Ionic 应用。

本书所需的内容

要开始构建 Ionic 应用,您需要对 Web 技术、TypeScript 和 Angular 有基本的了解。对移动应用开发、设备原生功能和 Cordova 的良好了解是可选的。

您需要安装 Node.js、Cordova CLI 和 Ionic CLI 才能使用 Ionic 框架。如果您想要使用设备功能,如相机或蓝牙,您需要在您的设备上设置移动操作系统。

本书旨在帮助那些想要学习如何使用 Ionic 构建移动混合应用程序的人。它也非常适合想要使用 Ionic 应用程序主题、集成 REST API,并了解更多关于设备功能(如相机、蓝牙)的人。

对 Angular 的先前了解对于成功完成本书至关重要。

约定

在本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:“TypeScript 文件保存为.ts扩展名。”

代码块设置如下:

x = 20; 
// after a few meaningful minutes  
x = 'nah! It's not a number any more';

任何命令行输入或输出都以以下方式编写:

npm install -g @angular/cli

新术语重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会在文本中出现,就像这样:“我们将编写三种方法,一种用于获取随机 gif,一种用于获取最新趋势,一种用于使用关键字搜索 Gif API。”

警告或重要说明以以下方式显示。

提示和技巧会以这种方式出现。

第一章:Angular - 入门

当 Timothy Berners-Lee 爵士发明互联网时,他从未想到互联网会被用来发布自拍照、分享猫视频或用广告轰炸网页。他的主要意图(猜测)是创建一个文档网络,以便互联网上的用户可以从任何地方访问这些超文本并加以利用。

Sitepoint 的 Craig Buckler 发表的一篇有趣的文章,标题为《网络磁盘空间不足》(www.sitepoint.com/web-runs-disk-space/),展示了互联网上的内容是如何分布的:

  • 28.65%的猫图片

  • 16.80%的自恋自拍

  • 14.82%毫无意义的社交媒体闲聊

  • 12.73%愚蠢的视频博主视频

  • 9.76%的广告/点击诱导页面

  • 8.70%的欺诈和骗局

  • 4.79%的虚假统计文章

  • 3.79%的新 JavaScript 工具/库

  • 0.76%的文件,以改善人类知识

您可以看到,从互联网的发明到现在,我们是如何演变的。更好的演变需要更好的框架来构建和管理这样的应用程序,这些应用程序需要可扩展、可维护和可测试。这就是 2010 年 Angular 填补空白的地方,自那时以来它一直在不断发展。

我们将从理解 Angular 的新变化、TypeScript 的重要性开始我们的旅程,并看看 Ionic 2 如何与 Angular 一起适应,以帮助构建性能高效和现代的移动混合应用程序。

在本章中,我们将通过一个示例快速了解 Angular 的新主题。Angular(2)中发生的主要变化主要是性能和组件化,除了语言更新。在本章中,我们将介绍以下主题:

  • Angular 有什么新东西?

  • TypeScript 和 Angular

  • 构建 Giphy 应用程序

Angular 有什么新东西?

Angular 2 是我见过的软件最受期待和最戏剧性的版本升级之一。Angular 1 对于 Web/移动 Web/混合应用程序开发人员来说是一个福音,它使许多事情变得容易。Angular 1 不仅帮助重构客户端应用程序开发,而且提供了构建应用程序的平台;不是网站,而是应用程序。尽管第一个版本在处理大型数据集时存在性能问题,但 Angular 团队在随后的 Angular 1.4.x 及以上版本中取得了相当大的进展,并通过发布更稳定的版本(即 Angular 2)解决了这些性能问题。

一些伴随 Angular(2)的新变化是:

  • 速度和性能改进。

  • 基于组件(而不是典型的 MV*)。

  • Angular CLI。

  • 简单而富有表现力的语法。

  • 渐进式 Web 应用程序(PWA)。

  • 跨平台应用程序开发,包括桌面、移动和 Web。

  • 基于 Cordova 的混合应用程序开发。

  • 用于快速初始视图的 Angular Universal 提供程序。

  • 升级以获得更好的动画、国际化和可访问性。

  • Angular 可以用 ES5、ES6、TypeScript 和 Dart 编写,根据用户对 JavaScript 口味的喜好。

有了这些新的更新,无论是在桌面、移动还是移动混合环境上,开发应用程序都变得更加容易。

注意:最新版本的 Angular 将被称为 Angular,而不是 Angular 2,或 AngularJS 4,或 NG4。因此,在本书中,我将把 Angular 版本 2 称为 Angular。

目前最新版本的 Angular 是 4。请查看第十一章,Ionic 3,了解更多关于 Angular 4 及其如何改进 Ionic 的信息。

您可以在这里找到有关 Angular 的更多信息:angular.io

注意:如果您是 Angular 的新手,可以参考这些书籍:

www.packtpub.com/web-development/learning-angular-2

www.packtpub.com/web-development/mastering-angular-2-components

www.packtpub.com/web-development/mastering-angular-2

www.packtpub.com/web-development/angular-2-example

或者这些视频:

www.packtpub.com/web-development/angular-2-projects-video

www.packtpub.com/web-development/web-development-angular-2-and-bootstrap-video

www.packtpub.com/web-development/angular-2-web-development-TypeScript-video

TypeScript 入门

Angular 在应用程序开发中广泛使用 TypeScript。因此,作为 Angular 入门的一部分,我们也将复习必要的 TypeScript 概念。

如果你是 TypeScript 的新手,TypeScript 是 JavaScript 的一种带类型的超集,可以编译成普通的 JavaScript。TypeScript 提供静态类型、类和接口,并支持几乎所有 ES6 和 ES7 的特性,这些特性在浏览器中还没有实现。

TypeScript 文件保存为.ts扩展名。

为无类型语言(JavaScript)添加类型的主要优势是让 IDE 理解我们尝试做的事情,并在编码时更好地帮助我们;换句话说,智能感知。

说到这一点,这就是我们可以用 TypeScript 做的事情。

变量类型

在纯 JavaScript 中,我们会做类似这样的事情:

x = 20; 
// after a few meaningful minutes  
x = 'nah! It's not a number any more';

但是在 TypeScript 中,我们不能像前面的代码片段中所示那样做,TypeScript 编译器会抱怨,因为我们在运行时修改了变量类型。

定义类型

当我们声明变量时,可以选择声明变量的类型。例如:

name: string = 'Arvind'; 
age: number  = 99; 
isAlive: boolean = true; 
hobbies: string[]; 
anyType: any; 
noType = 50; 
noType = 'Random String';

这增加了我们尝试做的事情的可预测性。

我是一个相信 JavaScript 是基于对象的编程语言而不是面向对象编程语言的人,我知道有很多人不同意我的观点。

在纯 JavaScript 中,我们有函数,它们就像类,并展示基于原型的继承。在 TypeScript/ES6 中,我们有类构造:

class Person { 
  name: string; 

constructor(personName: string) {  
this.name = personName;  
} 

getName { 
    return "The Name: " + this.greeting; 
}   
} 
// somewhere else 
arvind:Person = new Person('Arvind');

在上面的例子中,我们定义了一个名为 Person 的类,并定义了类构造函数,在类初始化时接受名称。

要初始化类,我们将使用 new 关键字调用类,并将名称传递给构造函数。存储类实例的变量——在上面的例子中是对象arvind,也可以被赋予类的类型。这有助于更好地理解arvind对象的可能性。

注意:ES6 中的类仍然遵循基于原型的继承,而不是经典的继承模型。

接口

当我们开始构建复杂的应用程序时,通常会需要一种特定类型的结构在整个应用程序中重复出现,这遵循某些规则。这就是接口的作用。接口提供结构子类型鸭子类型来检查实体的类型和形状

例如,如果我们正在开发一个涉及汽车的应用程序,每辆汽车都有一定的共同结构,在应用程序中使用时需要遵守这个结构。因此,我们创建一个名为 ICar 的接口。任何与汽车相关的类都将按照以下方式实现这个接口:

Interface ICar { 
  engine : String; 
  color: String; 
  price : Number; 
} 

class CarInfo implements ICar{ 
  engine : String; 
  color: String; 
  price : Number; 

  constructor(){ /* ... */} 
}

模块和导入

在纯 JavaScript 中,你可能会观察到这样的代码块:

(function(){ 
  var x = 20; 
  var y = x * 30; 
})(); //IIFE 
// x & y are both undefined here.

在 ES6/TS 中,使用导入和导出语法实现模块:

logic.ts
export function process(){ 
  x = 20; 
  y = x * 30; 
} 

exec.ts 
import { process } from './logic'; 
process();

这些是我们开始使用 TypeScript 所需的基本要素。我们将在需要时查看更多类似的概念。

通过这些概念,我们结束了开始使用 TypeScript 所需的关键概念。让我们开始学习 Angular。

有关 TypeScript 的更多信息,请查看:www.typescriptlang.org/docs/tutorial.html。还可以查看 TypeScript 介绍视频:channel9.msdn.com/posts/Anders-Hejlsberg-Introducing-TypeScript

Angular

Angular(2)添加了许多新功能,并更新了现有功能,并删除了一些 Angular 1.x 中的功能。在本节中,我们将介绍一些 Angular 的基本功能。

组件

Angular 组件受到 Web 组件规范的启发。在非常高的层面上,Web 组件有四个部分:

  • 自定义元素:用户可以创建自己的 HTML 元素。

  • HTML 导入:将一个 HTML 文档导入到另一个 HTML 文档中。

  • 模板:自定义元素的 HTML 定义。

  • Shadow DOM:编写自定义元素封装逻辑的规范。

前面四个规范解释了前端开发人员如何开发自己的独立、隔离和可重用组件,类似于 HTML 选择框(<select></select>)、文本区域(<textarea></textarea>)或输入框(<input />)。

您可以在此处阅读有关 Web 组件规范的更多信息:www.w3.org/standards/techs/components#w3c_all

如果您想深入了解 Web 组件,请查看:webcomponents.org/

如前所述,Angular(宽松地)是构建在 Web 组件上的,前面四个规范是以 Angular 方式实现的。

简单来说,我们整个应用程序是一个组件树。例如,如果我们看世界上最受欢迎的页面www.google.com,它可能看起来像这样:

如果我们必须在 Angular 中构建此页面,我们首先会将页面拆分为组件。

前面页面中的所有组件的可视表示如下:

注意:每个黑色框都是(自定义)组件。

从前面的图中可以看出,整个页面是一棵自定义组件树。

(自定义)组件通常由三部分组成:

  • component.ts:表示组件逻辑

  • component.html:表示组件视图(模板)

  • component.css:表示组件特定的样式

要构建自定义组件,我们需要在类的顶部使用Component装饰器。简单来说,装饰器让我们可以在类上配置特定的元数据。然后 Angular 将使用这些元数据来理解该类的行为。装饰器以@开头,后面跟着装饰器的名称。

组件装饰器告诉 Angular 正在处理的类需要表现出 Angular 组件的行为。一个简单的装饰器如下所示:

@Component({ 
  selector: 'app-root', 
  templateUrl: './app.component.html', 
  styleUrls: ['./app.component.css'] 
}) 
export class AppComponent { 
  // This is where we write the component logic! 
  title = 'Hello World!'; 
}

组件装饰器中包含的一些属性有:

  • selector:在模板中标识此组件的 CSS 选择器

  • templateUrl:包含视图模板的外部文件的 URL

  • styleUrls:要应用于此组件视图的样式表的 URL 列表

  • providers:此组件及其子组件可用的提供者列表

要了解有关 Component 装饰器的更多信息,请参阅以下链接:angular.io/docs/ts/latest/api/core/index/Component-decorator.html

区域

区域是 Angular 中引入的新概念之一。区域的概念是从 Dart 迁移到 JavaScript 的。

许多开发人员最初被 Angular 吸引的主要原因是其自动数据绑定,以及其他一些原因。这是通过在 Angular 1.x 中使用作用域来实现的。在 Angular 2 中,我们使用 Zone.js(github.com/angular/zone.js)来实现相同的功能。

每当数据发生变化时,Angular 会使用新数据更新适当的利益相关者(变量、接口、提供程序等)。Angular 可以轻松跟踪所有同步活动。但是对于异步代码的变化检测,例如事件处理、AJAX 调用或计时器,Angular 2 使用 Zone.js。

要了解有关区域的更多信息,以及它们的工作方式和在 Angular 中的变化检测,请查看 Angular 中的区域:blog.thoughtram.io/angular/2016/02/01/zones-in-angular-2.html和解释 Angular 变化检测:blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html

模板

模板用于将组件逻辑绑定到 HTML。模板还用作用户交互和应用逻辑之间的接口。

与 Angular 1 版本相比,模板已经发生了相当大的变化。但是仍然有一些事情保持不变。例如,我们从组件中获取值并在用户界面中显示它的方式仍然相同,使用双大括号表示法(插值语法)。

以下是一个app.component.ts的示例:

@Component({ 
  selector: 'app-root', 
  templateUrl: './app.component.html', 
  styleUrls: ['./app.component.css'] 
}) 
export class AppComponent { 
  // This is where we write the component logic! 
  title = 'Hello World!'; 
}

app.component.html可能如下所示:

<h1>
{{title}} <!-- This value gets bound from app.component.ts -->
</h1>

模板也可以通过将模板元数据传递给装饰器而不是templateUrl来内联。这可能如下所示:

 @Component({ 
  selector: 'app-root', 
  template: '<h1>{{title}}</h1>', 
  styleUrls: ['./app.component.css'] 
}) 
export class AppComponent { 
  // This is where we write the component logic! 
  title = 'Hello World!'; 
}

template元数据优先级高于templateUrl。例如,如果我们同时定义了templatetemplateUrl元数据,将选择并呈现template

我们还可以使用反引号()而不是引号在 ES6 和 TypeScript 中编写多行模板。有关更多信息,请参阅模板文字:[developer.mozilla.org/en/docs/Web/JavaScript/Reference/Template_literals`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Template_literals)

在 Angular 1.x 中,我们有核心/自定义指令。但是在 Angular(2)中,我们有各种表示法,使用这些表示法可以实现与 Angular 1 中指令相同的行为。

例如,如果我们想根据表达式的真值向元素添加自定义类,它会是这样的:

<div [class.highlight]="shouldHighlight">Hair!</div>

上述是著名的ng-class Angular 1.x 指令的替代品。

为了处理事件,我们使用( )表示法,如下所示:

<button (click)=pullHair($event)">Pull Hair</button>

而且pullhair()是在组件类内部定义的。

为了保持数据绑定最新,我们使用[( )]表示法,如下所示:

<input type="text" [(ngModel)]="name">

这使得组件类中的名称属性与文本框同步。

这里显示了*ngFor的示例,它是ng-repeat的替代品:

<ul> 
  <li *ngFor="let todo in todos">{{todo.title}}</li> 

</ul>

请注意,在todo前面的let表示它是该区域中的局部变量。

这些是我们需要开始实际示例的基本概念。当这些概念在我们的应用中出现时,我会谈论其他 Angular(2)的概念。

Giphy 应用

利用我们迄今为止学到的概念,我们将使用 Angular 和一个名为 Giphy 的开放 JSON API 提供程序构建一个简单的应用。

Giphy(giphy.com)是一个简单的 Gif 搜索引擎。Giphy 的人们公开了一个我们可以使用和处理数据的开放 REST API。

我们要构建的应用将与 Giphy JSON API 通信并返回结果。使用 Angular,我们将为应用中的三个功能构建接口:

  • 显示一个随机 Gif

  • 显示趋势 Gifs

  • 搜索 Gif

我们将使用 Angular CLI(cli.angular.io/)和 Twitter Bootstrap(getbootstrap.com/)与 Cosmos 主题(bootswatch.com/cosmo/)。

在我们开始构建应用之前,让我们首先了解应用的结构。

架构

我们要看的第一件事是应用程序的架构。在客户端,我们将有一个路由器,所有事情都将从那里开始流动。路由器将有四个路由:

  • 主页路由

  • 浏览路由

  • 搜索路由

  • 页面未找到路由

我们将有一个服务,其中有三种方法将与 Giphy REST API 交互。

除了前面提到的项目,我们还将有以下组件:

  • 导航组件:应用程序导航栏

  • 主页组件:主页,显示随机 gif

  • 趋势组件:显示趋势 gif

  • 搜索组件:搜索 gif

  • Giphy 组件:gif 模板

  • 页面未找到组件:显示告诉用户未找到任何内容的页面

此应用程序的组件树如下所示:

API

Giphy API 相当容易理解和使用。您可以在这里找到官方 API 文档:github.com/Giphy/GiphyAPI

我们将要使用的 API 是:

您可以转到上述链接以查看示例数据。

在撰写本文时,Giphy 公开了dc6zaTOxFJmzC作为要使用的 API 密钥。

Angular CLI

为了开发我们的 Giphy 应用程序,我们将使用 Angular CLI。如果您对 CLI 及其功能不熟悉,我建议您观看此视频:使用 Angular CLI 创建简单的 Angular 2 应用程序:www.youtube.com/watch?v=QMQbAoTLJX8

此示例是使用 Angular CLI 版本 1.0.0-beta.18 编写的。

安装软件

为了成功开发 Angular-Giphy 应用程序,我们需要安装 Node.js (nodejs.org/en)。我们将使用 NPM (www.npmjs.com) 通过 Angular CLI 下载所需的模块。

安装 Node.js 后,打开新的命令提示符/终端,然后运行以下命令:

npm install -g @angular/cli

这将继续安装 Angular CLI 生成器。这是我们开始开发应用程序所需的全部内容。

注意:我使用了 angular-cli 版本 1.0.0 构建此应用程序。

文本编辑器

关于文本编辑器,您可以使用任何编辑器来处理 Angular 和 Ionic。您还可以尝试 Sublime text (www.sublimetext.com/3) 或 Atom 编辑器 (atom.io/) 或 Visual Studio Code (code.visualstudio.com/) 来处理代码。

如果您使用 Sublime text,可以查看:github.com/Microsoft/TypeScript-Sublime-Plugin 以在编辑器中添加 TypeScript 智能。对于 Atom,请参阅以下链接:atom.io/packages/atom-TypeScript

搭建一个 Angular 2 应用程序

首先,我们要做的是使用 Angular CLI 搭建一个 Angular 应用程序。创建一个名为chapter1的新文件夹,并在该文件夹中打开命令提示符/终端,然后运行以下命令:

ng new giphy-app

现在,Angular CLI 生成器将继续创建所有必要的文件和文件夹,以便与我们的 Angular 应用程序一起使用。

如前所述,您可以查看使用 Angular CLI 创建简单的 Angular 2 应用程序:www.youtube.com/watch?v=QMQbAoTLJX8,也可以查看 Angular CLI 文档:cli.angular.io/reference.pdf 了解更多信息。

脚手架项目结构如下所示:

. 
├── .angular-cli.json 
├── .editorconfig 
├── README.md 
├── e2e 
│   ├── app.e2e-spec.ts 
│   ├── app.po.ts 
│   ├── tsconfig.e2e.json 
├── karma.conf.js 
├── node_modules 
├── package.json 
├── protractor.conf.js 
├── src 
│   ├── app 
│   │   ├── app.component.css 
│   │   ├── app.component.html 
│   │   ├── app.component.spec.ts 
│   │   ├── app.component.ts 
│   │   ├── app.module.ts 
│   ├── assets 
│   │   ├── .gitkeep 
│   ├── environments 
│   │   ├── environment.prod.ts 
│   │   ├── environment.ts 
│   ├── favicon.ico 
│   ├── index.html 
│   ├── main.ts 
│   ├── polyfills.ts 
│   ├── styles.css 
│   ├── test.ts 
│   ├── tsconfig.app.json 
│   ├── tsconfig.spec.json 
│   ├── typings.d.ts 
├── tsconfig.json 
├── tslint.json

我们将大部分时间花在src文件夹内。一旦项目完全搭建好,进入giphy-app文件夹并运行以下命令:

ng serve

这将启动内置服务器。构建完成后,我们可以导航到localhost:4200查看页面。页面应该看起来像这样:

构建 Giphy 应用程序

现在我们已经准备好开始了,我们将首先向应用程序添加 Twitter Bootstrap CSS。

在这个例子中,我们将使用来自bootswatch.com/的 Bootstrap 主题 Cosmos。我们可以在主题页面上找到 Cosmos CSS 主题:bootswatch.com/cosmo/,点击 Cosmos 下拉菜单,选择bootstrap.min.css选项。或者,我们也可以在这里找到它:bootswatch.com/cosmo/bootstrap.min.css

如果你愿意,你也可以使用任何其他主题或原始的 Bootstrap CSS。

要添加主题文件,导航到giphy-app/src/styles.css并在其中添加以下行:

@import "https://bootswatch.com/cosmo/bootstrap.min.css";

就是这样,现在我们的应用程序已经使用了 Twitter Bootstrap CSS。

接下来,我们将开始处理应用程序的主页面。为此,我们将利用 Twitter Bootstrap 的一个示例模板,名为 Starter Template。模板可以在这里找到:getbootstrap.com/examples/starter-template/

起始模板包括一个导航栏和一个主体部分,其中显示内容。

对于导航栏部分,我们将生成一个名为nav-bar的新组件,并更新其中的相关代码。

要使用 Angular CLI 生成一个新的自定义组件,导航到giphy-app文件夹并运行以下命令:

ng generate component nav-bar

注意:你可以终止当前运行的命令,或者生成一个新的命令提示符/终端来运行前面的命令。

你应该看到类似这样的东西:

create src/app/nav-bar/nav-bar.component.css
create src/app/nav-bar/nav-bar.component.html
create src/app/nav-bar/nav-bar.component.spec.ts
create src/app/nav-bar/nav-bar.component.ts
update src/app/app.module.ts

现在打开giphy-app/src/app/nav-bar/nav-bar.component.html并更新如下:

<nav class="navbar navbar-inverse navbar-fixed-top"> 
    <div class="container"> 
        <div class="navbar-header"> 
            <a class="navbar-brand" [routerLink]="['/']">Giphy App</a> 
        </div> 
        <div id="navbar" class="collapse navbar-collapse"> 
            <ul class="nav navbar-nav"> 
                <li [routerLinkActive]="['active']"><a [routerLink]="
                  ['/trending']">Trending</a></li> 
                <li [routerLinkActive]="['active']"><a [routerLink]="
                  ['/search']">Search</a></li> 
            </ul> 
        </div> 
    </div> 
</nav>

我们在这里所做的一切就是创建一个带有两个菜单项和应用程序名称的标题栏,它作为指向主页的链接。

接下来,我们将更新giphy-app/src/app/app.component.html以加载nav-bar组件。用以下内容替换该文件的内容:

<nav-bar></nav-bar>

接下来,我们将开始向应用程序添加路由。如前所述,我们将有三个路由。

为了为当前应用程序添加路由支持,我们需要做三件事:

  1. 创建所需的路由。

  2. 配置@NgModule

  3. 告诉 Angular 在哪里加载这些路由的内容。

在撰写本文时,Angular CLI 已禁用了路由生成。因此,我们将手动创建相同的路由。否则,我们可以简单地运行ng generate route home来生成主页路由。

所以首先,让我们定义所有的路由。在 app 文件夹内创建一个名为app.routes.ts的新文件。更新文件如下:

import { HomeComponent } from './home/home.component'; 
import { TrendingComponent } from './trending/trending.component'; 
import { SearchComponent } from './search/search.component'; 
import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; 

export const ROUTES = [ 
  { path: '', component: HomeComponent }, 
  { path: 'trending', component: TrendingComponent }, 
  { path: 'search', component: SearchComponent }, 
  { path: '**', component: PageNotFoundComponent } 
];

我们所做的一切就是导出一个路由数组。请注意路径'**'。这是我们定义路由的另一部分。

现在我们将创建所需的组件。运行以下命令:

ng generate component home
ng generate component trending
ng generate component search
ng generate component pageNotFound

接下来,我们将配置@NgModule。打开giphy-app/src/app/app.module.ts并在顶部添加以下导入:

import { RouterModule }   from '@angular/router'; 
import { ROUTES } from './app.routes';

接下来,更新@NgModule装饰器的imports属性如下:

//.. snipp 
imports: [ 
    BrowserModule, 
    FormsModule, 
    HttpModule, 
    RouterModule.forRoot(ROUTES) 
  ], 
//.. snipp 

完成的页面将如下所示:

import { BrowserModule } from '@angular/platform-browser'; 
import { NgModule } from '@angular/core'; 
import { FormsModule } from '@angular/forms'; 
import { HttpModule } from '@angular/http'; 
import { RouterModule }   from '@angular/router'; 

import { AppComponent } from './app.component'; 
import { NavBarComponent } from './nav-bar/nav-bar.component'; 
import { HomeComponent } from './home/home.component'; 
import { TrendingComponent } from './trending/trending.component'; 
import { SearchComponent } from './search/search.component'; 
import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; 

import { ROUTES } from './app.routes'; 

@NgModule({ 
  declarations: [ 
    AppComponent, 
    NavBarComponent, 
    HomeComponent, 
    TrendingComponent, 
    SearchComponent, 
    PageNotFoundComponent 
  ], 
  imports: [ 
    BrowserModule, 
    FormsModule, 
    HttpModule, 
    RouterModule.forRoot(ROUTES) 
  ], 
  providers: [], 
  bootstrap: [AppComponent] 
}) 
export class AppModule { }

现在我们将更新应用程序组件以显示导航栏以及当前路由内容。

更新giphy-app/src/app/app.component.html如下:

<app-nav-bar></app-nav-bar> 
<router-outlet></router-outlet>

使用router-outlet,我们告诉路由器在该位置加载当前路由内容。

如果你想了解更多关于 Angular 中的路由,请查看:Brian Ford 的《Eleven Dimensions with Component Router》:www.youtube.com/watch?v=z1NB-HG0ZH4

接下来,我们将更新主页组件的 HTML 并测试到目前为止的应用程序。

打开giphy-app/src/app/home/home.component.html并按以下方式更新它:

<div class="container"> 
    <div class="starter-template"> 
        <h1>Giphy App</h1> 
        <p class="lead">This app uses the JSON API provided by Giphy to Browse and Search Gifs. 
            <br> To know more checkout : <a href="https://github.com/Giphy/GiphyAPI#trending-gifs-endpoint">Giphy API</a> </p> 
    </div> 
</div>

完成后,保存文件并运行以下命令:

ng  serve

我们应该看到以下页面:

如我们所见,页面看起来有问题。让我们通过添加一些样式来修复这个问题。打开giphy-app/src/styles.css并添加以下内容:

body {
  padding-top: 50px; 
  padding-bottom: 20px; 
} 

.starter-template { 
  padding: 40px 15px; 
  text-align: center; 
}

现在我们的页面将如预期般显示:

接下来,我们将开始编写服务以与 Giphy API 交互。我们将编写三种方法,一种用于获取随机 gif,一种用于获取最新趋势,一种用于使用关键字搜索 Gif API。

开始时,我们将生成一个服务。运行以下命令:

ng generate service giphy

WARNING Service is generated but not provided, it must be provided to be used

如警告所示,生成的服务尚未标记为提供者。因此,我们需要手动进行标记。

打开giphy-app/src/app/app.module.ts并导入GiphyService

import { GiphyService } from './giphy.service';

接下来,在@NgModule装饰器的providers属性中添加GiphyService作为提供者:

//.. snipp 
providers: [ 
    GiphyService 
  ], 
//..snipp

完整的giphy-app/src/app/app.module.ts如下所示:

import { BrowserModule } from '@angular/platform-browser'; 
import { NgModule } from '@angular/core'; 
import { FormsModule } from '@angular/forms'; 
import { HttpModule } from '@angular/http'; 
import { RouterModule }   from '@angular/router'; 

import { AppComponent } from './app.component'; 
import { NavBarComponent } from './nav-bar/nav-bar.component'; 
import { HomeComponent } from './home/home.component'; 
import { TrendingComponent } from './trending/trending.component'; 
import { SearchComponent } from './search/search.component'; 
import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; 

import { ROUTES } from './app.routes'; 

import { GiphyService } from './giphy.service'; 

@NgModule({ 
  declarations: [ 
    AppComponent, 
    NavBarComponent, 
    HomeComponent, 
    TrendingComponent, 
    SearchComponent, 
    PageNotFoundComponent 
  ], 
  imports: [ 
    BrowserModule, 
    FormsModule, 
    HttpModule, 
    RouterModule.forRoot(ROUTES) 
  ], 
  providers: [ 
    GiphyService 
  ], 
  bootstrap: [AppComponent] 
}) 
export class AppModule { }

现在我们将更新giphy-app/src/app/giphy.service.ts以包含这三种方法。打开giphy-app/src/app/giphy.service.ts并按以下方式更新它:

import { Injectable } from '@angular/core'; 
import { Http, Response, Jsonp } from '@angular/http'; 
import { Observable } from 'rxjs/Rx'; 
import 'rxjs/Rx'; 

@Injectable() 
export class GiphyService { 
  private giphyAPIBase = 'http://api.giphy.com/v1/gifs'; 
  private APIKEY = 'dc6zaTOxFJmzC'; 

  constructor(private http: Http) { } 

  getRandomGif(): Observable<Response> { 
    return this.http.get(this.giphyAPIBase + 
      '/random?api_key=' + this.APIKEY) 
      .map((res) => res.json()); 
  } 

  getTrendingGifs(offset, limit): Observable<Response> { 
    return this.http.get(this.giphyAPIBase + 
      '/trending?api_key=' + this.APIKEY + '&offset=' + offset + 
      '&limit=' + limit) 
      .map((res) => res.json()); 
  } 

  searchGifs(offset, limit, text): Observable<Response> { 
    return this.http.get(this.giphyAPIBase + '/search?api_key=' + 
      this.APIKEY + '&offset=' + offset + 
      '&limit=' + limit + '&q=' + text) 
      .map((res) => res.json()); 
  } 
}

我们所做的只是向相应的 Giphy API URL 发出 HTTP GET 请求并返回一个 Observable。

在 RxJS(reactivex.io/rxjs/)中,Observable 是一个可以随时间变化的实体。这是 RxJS 的最基本构建块。观察者订阅 Observable 并对其变化做出反应。这种模式称为响应式模式。

引用自文档:

这种模式有助于并发操作,因为它不需要在等待 Observable 发出对象时阻塞,而是创建一个观察者作为哨兵,随时准备在 Observable 未来发出对象时做出适当的反应。

如果您对 Observables 还不熟悉,可以从这里开始:reactivex.io/documentation/observable.html,然后阅读:在 Angular 中利用 Observables:blog.thoughtram.io/angular/2016/01/06/taking-advantage-of-observables-in-angular2.html和 Angular 2 中使用 Observables 进行 HTTP 请求:scotch.io/tutorials/angular-2-http-requests-with-observables

现在服务已经完成,我们将更新HomeComponent以获取一个随机的 gif 并在主页上显示它。

打开giphy-app/src/app/home/home.component.ts并按以下方式更新它:

import { Component, OnInit } from '@angular/core'; 
import { GiphyService } from '../giphy.service'; 

@Component({ 
  selector: 'app-home', 
  templateUrl: './home.component.html', 
  styleUrls: ['./home.component.css'] 
}) 
export class HomeComponent implements OnInit { 
  public gif: string; 
  public result: any; 
  public isLoading: boolean = true; 

  constructor(private giphyService: GiphyService) { 
    this.getRandomGif(); 
  } 

  ngOnInit() { 
  } 

  getRandomGif() { 
    this.giphyService.getRandomGif().subscribe( 
      (data) => { 
        this.result = data; 
        this.gif = this.result.data.image_url; 
        this.isLoading = false; 
      }, 
      (err) => console.log('Oops!', err), 
      () => console.log('Response', this.result) 
    ) 
  } 
}

在上述代码中,首先,我们导入了GiphyService并将其添加到构造函数中。接下来,我们编写了getRandomGif()并从构造函数中调用了getRandomGif()。在getRandomGif()中,我们在giphyService上调用了getRandomGif()来获取一个随机的 gif。然后,我们将 gif 赋值给一个名为gif的类变量。

为了确保一切正常运行,我们将通过执行ng serve并打开开发者工具来运行应用程序。如果一切顺利,我们应该能看到来自 Giphy API 的响应:

现在我们已经得到了响应,我们希望构建一个组件来显示这个 gif。我们希望构建一个单独的组件,因为我们将在其他页面上使用相同的组件来显示需要的 gif。

让我们继续搭建组件。运行以下命令:

ng generate component gif-viewr

接下来,打开giphy-app/src/app/gif-viewr/gif-viewr.component.html并按以下方式更新它:

<div class="item"> 
  <div class="well"> 
    <img src="img/{{imgUrl}}"> 
  </div> 
</div>

完成后,我们需要告诉组件从父组件中期望数据,因为主页组件将把imgUrl传递给gif-viewer组件。

打开giphy-app/src/app/gif-viewr/gif-viewr.component.ts。首先,通过添加对 Input 装饰器的引用来更新导入语句:

import { Component, OnInit, Input} from '@angular/core';

接下来,在imgUrl变量中添加一个 Input 装饰器:

@Input() imgUrl: string;

更新后的giphy-app/src/app/gif-viewr/gif-viewr.component.ts如下所示:

import { Component, OnInit, Input} from '@angular/core'; 

@Component({ 
  selector: 'app-gif-viewr', 
  templateUrl: './gif-viewr.component.html', 
  styleUrls: ['./gif-viewr.component.css'] 
}) 
export class GifViewrComponent implements OnInit { 
  @Input() imgUrl: string; 

  constructor() { } 

  ngOnInit() { 
  } 
}

注意:要为组件定义输入,我们使用@Input装饰器。要了解更多关于@Input装饰器的信息,请参考 Angular 文档中的属性指令部分:angular.io/docs/ts/latest/guide/attribute-directives.html

保存文件并打开giphy-app/src/app/home/home.component.html。我们将在此页面内添加app-gif-viewr组件:

<app-gif-viewr class="home" [imgUrl]="gif"></app-gif-viewr>

完整的文件如下所示:

<div class="container"> 
    <div class="starter-template"> 
        <h1>Giphy App</h1> 
        <p class="lead">This app uses the JSON API provided by Giphy to 
          Browse and Search Gifs. 
            <br> To know more checkout : 
            <a href=
            "https://github.com/Giphy/GiphyAPI#trending-gifs-endpoint">
            Giphy API</a> </p> 
    </div> 

  <app-gif-viewr class="home" [imgUrl]="gif"></app-gif-viewr> 
</div>

接下来,我们将更新 CSS 以美化页面。打开giphy-app/src/styles.css并将以下 CSS 添加到现有样式中:

.home .well{ 
   width: 70%; 
    margin: 0 auto; 
} 

img{ 
  width: 100%; 
}

如果我们回到浏览器并刷新,我们应该会看到以下内容:

每次刷新页面,我们都会看到一个新的 gif 出现。

接下来,我们将在热门页面上进行工作。该页面将显示当前流行的 gif,使用 Pintrest 布局(或 Masonry 布局)。热门 REST API 支持分页。我们将利用这一点,每次加载 12 个 gif。然后提供一个“加载更多”按钮来获取接下来的 12 个 gif。

首先,让我们从 Giphy API 获取数据。打开giphy-app/src/app/trending/trending.component.ts。我们将首先导入GiphyService

import { GiphyService } from '../giphy.service';

现在,我们将添加相同的内容到构造函数中,并更新构造函数以调用getTrendingGifs()

constructor(private giphyService: GiphyService) { } 
In ngOnInit(), we will call the getTrendingGifs() API: 
  ngOnInit() { 
    this.getTrendingGifs(this.offset, this.perPage); 
  } 
Next, we will add the required class variables:  
private offset = 0; 
private perPage = 12; 
public results: any; 
public gifs: Array<any> = []; 
public isLoading: boolean = true;

offsetperPage将用于管理分页。

results将用于存储来自服务器的响应。

gifs是由一系列热门 gif 组成的数组,我们将其暴露给模板。

isLoading是一个boolean变量,用于跟踪请求是否正在进行中。使用isLoading,我们将显示/隐藏“加载更多”按钮。

接下来,我们将添加getTrendingGifs()

getTrendingGifs(offset, limit) { 
    this.giphyService.getTrendingGifs(offset, limit).subscribe( 
      (data) => { 
        this.results = data; 
        this.gifs = this.gifs.concat(this.results.data); 
        this.isLoading = false; 
      }, 
      (err) => console.log('Oops!', err), 
      () => console.log('Response', this.results) 
    ) 
  } 
And finally getMore(), which will be invoked by the Load More button: 
 getMore() { 
    this.isLoading = true; 
    this.offset = this.offset + this.perPage; 
    this.getTrendingGifs(this.offset, this.perPage); 
  }

为了显示检索到的 gif,我们将更新热门组件模板。打开giphy-app/src/app/trending/trending.component.html并进行如下更新:

<div class="container"> 
    <h1 class="text-center">Trending Gifs</h1> 
    <div class="wrapper"> 
        <app-gif-viewr [imgUrl]="gif.images.original.url" *ngFor="let gif of gifs"></app-gif-viewr> 
    </div> 
    <input type="button" value="Load More" class="btn btn-primary btn-block" *ngIf="!isLoading" (click)="getMore()"> 
</div>

我们在这里所做的一切就是设置app-gif-viewr以通过对其应用*ngFor指令来获取 gif URL。底部还有一个“加载更多”按钮,用户可以加载更多 gif。

最后,为了实现 Pintrest/Masonry 布局,我们将添加一些 CSS 规则。打开giphy-app/src/styles.css并添加以下样式:

*, *:before, *:after { 
  box-sizing: border-box !important; 
} 

.wrapper { 
  column-width: 18em; 
  column-gap: 1em; 
} 

.item { 
  display: inline-block; 
  padding: .25rem; 
  width: 100%; 
} 

.well { 
  position: relative; 
  display: block; 
}

保存所有文件并返回浏览器。如果我们点击导航栏中的热门菜单项,我们应该会看到以下内容:

如果我们完全向下滚动,我们应该会看到一个“加载更多”按钮:

点击“加载更多”按钮将加载下一组 gif:

我浪费了大约 15 分钟点击“加载更多”并观看 gif。我认为这就是为什么 API 应该有速率限制的原因。

最后,我们将实现搜索 gif。打开giphy-app/src/app/search/search.component.ts并导入GiphyService

import { GiphyService } from '../giphy.service';

在构造函数中将giphyService添加为一个类变量:

constructor(private giphyService: GiphyService) { }

接下来,我们将添加变量来管理分页以及响应:

  private offset = 0; 
  private perPage = 12; 
  public results: any; 
  public query: string; 
  public gifs: Array<any> = []; 
  public isLoading: boolean = true;

现在我们将调用searchGifs,它通过传递查询字符串来进行 REST 调用以获取搜索到的 gif:

searchGifs(offset, limit, query) { 
    this.giphyService.searchGifs(offset, limit, query).subscribe( 
      (data) => { 
        this.results = data; 
        this.gifs = this.gifs.concat(this.results.data); 
        this.isLoading = false; 
      }, 
      (err) => console.log('Oops!', err), 
      () => console.log('Response', this.results) 
    ) 
  }

以下是一个管理搜索表单提交按钮的方法:

  search(query) { 
    this.query = query; 
    this.isLoading = true; 
    this.searchGifs(this.offset, this.perPage, this.query); 
  }

最后,getMore()来加载同一查询的更多页面:

getMore() { 
    this.isLoading = true; 
    this.offset = this.offset + this.perPage; 
    this.searchGifs(this.offset, this.perPage, this.query); 
  }

更新后的giphy-app/src/app/search/search.component.ts如下所示:

import { Component, OnInit } from '@angular/core'; 
import { GiphyService } from '../giphy.service'; 

@Component({ 
  selector: 'app-search', 
  templateUrl: './search.component.html', 
  styleUrls: ['./search.component.css'] 
}) 
export class SearchComponent implements OnInit { 
  private offset = 0; 
  private perPage = 12; 
  public results: any; 
  public query: string; 
  public gifs: Array<any> = []; 
  public isLoading: boolean = true; 

  constructor(private giphyService: GiphyService) { } 

  ngOnInit() { 
  } 

  searchGifs(offset, limit, query) { 
    this.giphyService.searchGifs(offset, limit, query).subscribe( 
      (data) => { 
        this.results = data; 
        this.gifs = this.gifs.concat(this.results.data); 
        this.isLoading = false; 
      }, 
      (err) => console.log('Oops!', err), 
      () => console.log('Response', this.results) 
    ) 
  } 

  search(query) { 
    this.query = query; 
    this.isLoading = true; 
    this.searchGifs(this.offset, this.perPage, this.query); 
  } 

  getMore() { 
    this.isLoading = true; 
    this.offset = this.offset + this.perPage; 
    this.searchGifs(this.offset, this.perPage, this.query); 
  } 
}

现在我们将更新giphy-app/src/app/search/search.component.html。打开giphy-app/src/app/search/search.component.html并进行如下更新:

<div class="container"> 
    <h1 class="text-center">Search Giphy</h1> 
    <div class="row"> 
        <input class="form-control" type="text" placeholder="Search 
          something.. Like.. LOL or Space or Wow" #searchText 
          (keyup.enter)="search(searchText.value)"> 
    </div> 
    <br> 
    <div class="wrapper"> 
        <app-gif-viewr [imgUrl]="gif.images.original.url" *ngFor="let 
          gif of gifs"></app-gif-viewr> 
    </div> 
    <input type="button" value="Load More" class="btn btn-primary btn-block" *ngIf="!isLoading" (click)="getMore()"> 
</div>

这个视图与热门组件相同,只是有一个搜索文本框,允许用户通过输入字符串进行搜索。

如果我们保存所有文件,返回浏览器,并导航到搜索页面,我们应该会看到一个带有搜索文本框的空白页面。此时,“加载更多”按钮将不会显示。如果我们输入文本并按回车键,我们应该会看到结果,如下图所示:

有了这个,我们已经完成了在 Angular 应用中使用 Giphy API 的实现。

为了结束这个例子,我们将更新giphy-app/src/app/page-not-found/page-not-found.component.html如下:

<div class="container"> 
    <div class="starter-template"> 
        <h1>404 Not Found</h1> 
        <p class="lead">Looks Like We Were Not Able To Find What You Are Looking For. 
            <br>Back to : <a [routerLink]="['/']">Home</a>? </p> 
    </div> 
</div>

当我们导航到localhost:4200/nopage时,我们应该看到以下页面:

摘要

在本章中,我们已经对 TypeScript 进行了高层次的概述,以及为什么我们使用 TypeScript。接下来,我们熟悉了 Angular 的新语法和组件结构。利用这些知识,我们构建了一个名为 Giphy 的应用程序,它与 Giphy 的 REST API 进行交互以获取 gif。

您可以在这里阅读更多关于 Angular 的信息:angular.io

此外,查看第十一章,Ionic 3,了解有关 Angular 4 的更多变化。

在下一章--欢迎来到 Ionic,我们将开始使用 Cordova 进行移动混合开发,并了解 Ionic 如何融入更大的方案。

第二章:欢迎来到 Ionic

在上一章中,我们通过一个例子学习了 Angular 2。在本章中,我们将看一下移动混合应用的大局,设置所需的软件来开发 Ionic 应用,最后搭建一些应用并探索它们。

本章涵盖的主题如下:

  • 移动混合架构

  • Apache Cordova 是什么?

  • Ionic 是什么?

  • 设置开发和运行 Ionic 应用所需的工具

  • 使用 Ionic 模板

移动混合架构

在我们开始使用 Ionic 之前,我们需要了解移动混合开发的大局。

这个概念非常简单。几乎每个移动操作系统(在使用 Cordova 时也称为平台)都有一个用于开发应用程序的 API。这个 API 包括一个名为 WebView 的组件。WebView 通常是一个在移动应用程序范围内运行的浏览器。这个浏览器运行 HTML、CSS 和 JS 代码。这意味着我们可以使用上述技术构建一个网页,然后在我们的应用程序内执行它。

我们可以使用相同的 Web 开发知识来构建本地混合移动应用程序(这里,本地是指在打包后与资产一起安装在设备上的特定于平台的格式文件),例如:

  • Android 使用 Android 应用程序包(.apk

  • iOS 使用 iPhone 应用程序存档(.ipa

  • Windows Phone 使用应用程序包(.xap

包/安装程序由一段初始化网页和一堆显示网页内容所需的资产的本地代码组成。

在移动应用程序容器内显示网页的这种设置,其中包含我们的应用程序业务逻辑,被称为混合应用。

Apache Cordova 是什么?

简单来说,Cordova 是将 Web 应用程序和本地应用程序拼接在一起的软件。Apache Cordova 的网站表示:

“Apache Cordova 是使用 HTML、CSS 和 JavaScript 构建本地移动应用程序的平台。”

Apache Cordova 不仅仅是将 Web 应用程序与本地应用程序拼接在一起,而且还提供了一组用 JavaScript 编写的 API,以与设备的本地功能进行交互。是的,我们可以使用 JavaScript 访问我们的相机,拍照并通过电子邮件发送。听起来很激动人心,对吧?

为了更好地理解发生了什么,让我们看一下以下的截图:

正如我们所看到的,我们有一个 WebView,HTML/CSS/JS 代码在其中执行。这段代码可以是一个简单的独立用户界面;在最好的情况下,我们正在通过 AJAX 请求从远程服务器获取一些数据。或者,这段代码可以做更多的事情,比如与设备的蓝牙通信并获取附近设备的列表。

在后一种情况下,Cordova 有一堆 API,用 JavaScript 与 WebView 进行接口,然后以其本地语言(例如,Android 的 Java)与设备进行通信,从而在这种情况下为 Java 和 JavaScript 提供了桥梁。例如,如果我们想了解正在运行我们的应用程序的设备更多信息,我们只需要在 JS 文件中编写以下代码并启动应用程序:

var platform = device.platform;

安装设备插件后,我们还可以使用 JavaScript 从 WebView 内部访问设备的 UUID、型号、操作系统版本和 Cordova 版本,如下所示:

var uuid = device.uuid; 
var model = device.model; 
var version = device.version; 
var Cordova = device.Cordova;

我们将在第六章 Ionic Native中更多地处理 Cordova 插件。

前面的解释是为了让你了解移动混合应用的结构以及我们如何使用 JavaScript 从 WebView 中使用设备功能。

Cordova 不会将 HTML、CSS 和 JS 代码转换为特定于操作系统的二进制代码。它所做的只是包装 HTML、CSS 和 JS 代码,并在 WebView 内执行它。

所以你现在一定已经猜到了,Ionic 是我们用来构建在 WebView 中运行并与 Cordova 通信以访问设备特定 API 的 HTML/CSS/JS 代码的框架。

Ionic 2 是什么?

Ionic 2 是一个用于开发混合移动应用程序的美观的开源前端 SDK,提供了移动优化的 HTML、CSS 和 JS 组件,以及用于构建高度交互式应用程序的手势和工具。

与其他框架相比,Ionic 2 通过最小化 DOM 操作和硬件加速的转换,具有高性能效率。Ionic 使用 Angular 2 作为其 JavaScript 框架。

在像 Ionic 2 这样的框架中使用 Angular 的强大功能,可能性是无限的(只要在移动应用程序中有意义,我们可以在 Ionic 中使用任何 Angular 组件)。 Ionic 2 与 Cordova 的设备 API 集成非常好。这意味着我们可以使用 Ionic Native 访问设备 API,并将其与 Ionic 的美观用户界面组件集成。

Ionic 有自己的命令行界面(CLI)来搭建、开发和部署 Ionic 应用程序。在开始使用 Ionic CLI 之前,我们需要设置一些软件。

Ionic 3

在本书发布时,Ionic 的最新版本是 3。我已经准备了另一章名为 Ionic 3(第十一章),您可以参考了解更多关于 Ionic 3 及其变化的信息。

另外,请注意,本书中的示例在使用 Ionic 3 时仍然有效。可能会有一些语法和结构上的变化,但总体意思应该保持不变。

软件设置

现在我们将设置所有开发和运行 Ionic 应用程序所需的必要软件。

安装 Node.js

由于 Ionic 使用 Node.js 作为其 CLI 以及构建任务,我们将首先安装它如下:

  1. 导航到nodejs.org/

单击主页上的安装按钮,将自动下载适用于我们操作系统的安装程序。我们也可以导航到nodejs.org/download/并下载特定的副本。

  1. 通过执行下载的安装程序安装 Node.js。

要验证 Node.js 是否已成功安装,请打开新的终端(*nix 系统)或命令提示符(Windows 系统)并运行以下命令:

 node -v
 > v6.10.1

  1. 现在执行以下命令:
 npm -v
 > 3.10.10

npm是一个Node Package Manager,我们将使用它来下载我们 Ionic 项目的各种依赖项。

我们只需要在开发过程中使用 Node.js。指定的版本仅用于说明。您可能有相同版本或软件的最新版本。

安装 Git

Git 是一个免费的开源分布式版本控制系统,旨在处理从小型到非常大型的项目,并具有速度和效率。在我们的情况下,我们将使用一个名为 Bower 的包管理器,它使用 Git 来下载所需的库。此外,Ionic CLI 使用 Git 来下载项目模板。

要安装 Git,请导航到git-scm.com/downloads并下载适用于您平台的安装程序。安装成功后,我们可以导航到命令提示符/终端并运行以下命令:

git --version

我们应该看到以下输出:

> git version 2.11.0 (Apple Git-81)

文本编辑器

这是一个完全可选的安装。每个人都有自己喜欢的文本编辑器。在尝试了许多文本编辑器之后,我纯粹因为其简单性和插拔包的数量而爱上了 Sublime Text。

如果您想尝试这个编辑器,可以导航到www.sublimetext.com/3下载 Sublime Text 3。

因为我们将用 TypeScript 编写 JavaScript 代码,Microsoft 的 Visual Studio Code 是另一个不错的选择。

如果您想尝试这个编辑器,可以导航到code.visualstudio.com/

您也可以尝试 Atom 作为另一种选择。

如果您想尝试这个编辑器,可以导航到atom.io/

安装 TypeScript

接下来,我们将安装 TypeScript 编译器。如第一章“Angular - A Primer”中所述,我们将使用 TypeScript 编写 JavaScript 代码。要安装 TypeScript 编译器,请运行以下命令:

npm install typescript -g

一旦 TypeScript 成功安装,我们可以通过运行此命令来验证:

tsc -v
> message TS6029: Version 1.7.5

在 Ionic 3 发布时,TypeScript 的最新版本是 2.2.2。在使用 Ionic 3 时,您可能需要将 TSC 的版本更新为 2.2.2 或更高版本。

安装 Cordova 和 Ionic CLI

最后,为了完成 Ionic 2 的设置,我们将安装 Ionic 和 Cordova CLI。Ionic CLI 是 Cordova CLI 的包装器,具有一些附加功能。

本书中的所有代码示例使用 Cordova 版本 6.4.0,Ionic CLI 版本 2.1.14 和 Ionic 版本 2.1.17。但是最新版本的 Ionic 也应该可以使用相同的代码。

要安装 Ionic CLI,请运行以下命令:

npm install -g ionic cordova

要验证安装,请运行以下命令:

cordova -v
> 6.4.0

您也可以运行此命令:

ionic -v
> 2.1.14

您可以运行以下命令获取有关 Ionic 设置的完整信息:

ionic info

Your system information:
Cordova CLI: 6.4.0 
Ionic CLI Version: 2.1.14
Ionic App Lib Version: 2.1.7
ios-deploy version: 1.8.4 
ios-sim version: 5.0.6 
OS: macOS Sierra
Node Version: v6.10.1
Xcode version: Xcode 8.3 Build version 8E162

如果您看到的 Ionic CLI 版本大于或等于 2.2.2,则您有一个可以处理 Ionic 3 应用程序的 Ionic CLI。尽管如此,本书中的命令和示例将以相同的方式工作。

要了解 Ionic CLI 包含的功能,运行以下命令:

 ionic

我们应该看到一系列任务,如下截图所示:

除了在上述截图中看到的任务之外,还有一些其他任务。

我们可以阅读任务和解释,了解它们的作用。还要注意,截至今天,其中一些任务仍处于测试阶段。

通过这样,我们已经完成了使用 Ionic 开发应用所需的所有软件的安装。

平台指南

在本书结束时,我们将构建可以部署到设备上的应用程序。由于 Cordova 接受 HTML、CSS 和 JS 代码作为输入并生成特定于平台的安装程序,我们需要在我们的机器上有构建环境。

Android 用户可以按照 Android 平台指南中的说明在本地机器上设置 SDK:cordova.apache.org/docs/en/edge/guide_platforms_android_index.md.html#Android%2520Platform%2520Guide

iOS 用户可以按照 iOS 平台指南中的说明在本地机器上设置 SDK:cordova.apache.org/docs/en/edge/guide_platforms_ios_index.md.html#iOS%20Platform%20Guide

您需要 macOS 环境来开发 iOS 应用程序。

截至今天,Ionic 仅支持 Android 4.0+(尽管在 2.3 上也可以工作)和 iOS 6+移动平台。但 Cordova 支持更多平台。

您可以在以下网址查看其他支持的平台:cordova.apache.org/docs/en/edge/guide_platforms_index.md.html#Platform%20Guides

你好 Ionic

现在我们已经完成了软件设置,我们将创建一些 Ionic 应用程序的脚手架。

Ionic 有三个主要/常用模板,我们可以使用这些模板快速开始开发应用程序:

  • 空白:这是一个空白的 Ionic 项目,有一个页面

  • 选项卡:这是一个使用 Ionic 选项卡构建的示例应用程序

  • 侧边菜单:这是一个使用侧边菜单驱动导航的示例应用程序

为了了解脚手架的基础知识,我们将从空白模板开始。

为了保持我们的学习过程清晰,我们将创建一个文件夹结构来处理 Ionic 项目。创建一个名为chapter2的文件夹。

接下来,打开一个新的命令提示符/终端,并将目录(cd)更改为chapter2文件夹。现在运行以下命令:

ionic start -a "Example 1" -i app.example.one example1 blank --v2

上述命令具有以下功能:

  • -a "Example 1":这是应用程序的可读名称。

  • -i app.example.one:这是应用程序 ID/反向域名。

  • example1:这是文件夹的名称。

  • blank:这是模板的名称。

  • --v2:此标志表示项目将使用最新版本的 Ionic 进行脚手架。这可能会在将来被移除。

参考附录,附加主题和提示,了解更多关于 Ionic start 任务的信息。

Ionic CLI 在执行任务时非常冗长。正如我们从命令提示符/终端中所看到的,项目正在创建时会打印出大量信息。

首先,从ionic2-app-base GitHub 存储库github.com/driftyco/ionic2-app-base下载ionic2-app-base。之后,从ionic-starter-blank GitHub 存储库github.com/driftyco/ionic2-starter-blank下载ionic2-starter-blank。然后安装所有必需的依赖项。

一旦项目成功创建,我们将看到一堆关于如何进一步进行的说明。我们的输出应该看起来类似以下内容:

为了进一步进行,我们将使用cd命令导航到example1文件夹。我们不会按照命令提示符/终端中提供的说明进行,因为我们还没有理解项目设置。一旦我们对 Ionic 有一个大致的了解,我们可以在脚手架一个新的 Ionic 应用程序后,开始使用命令提示符/终端输出中提供的命令。

一旦我们已经切换到example1文件夹,我们将通过以下命令提供应用程序:

ionic serve

这将在端口8100上启动一个新的dev服务器,然后在我们的默认浏览器中启动应用程序。我强烈建议在使用 Ionic 时将 Google Chrome 或 Mozilla Firefox 设置为默认浏览器。

当浏览器启动时,我们应该看到空模板的主页。

如果我们运行ionic serve并且端口8100已被占用,Ionic 将在8101上启动应用程序。

我们还可以使用以下命令在任何其他端口上提供 Ionic 应用程序:

ionic serve -p 8200

一旦应用程序成功启动并且我们在浏览器中看到输出,我们将返回到命令提示符/终端,应该会看到类似以下截图的内容:

浏览器开发者工具设置

在我们进一步进行之前,我建议按照以下格式在浏览器中设置开发者工具。

Google Chrome

一旦 Ionic 应用程序启动,按下 Mac 上的Command + Option + I,或者在 Windows/Linux 上按下Ctrl + Shift + I,打开开发者工具。然后点击顶部行中倒数第二个图标,靠近关闭按钮,如下截图所示:

这将把开发者工具停靠在当前页面的一侧。拖动浏览器和开发者工具之间的分界线,直到视图开始类似于移动设备。

如果您在开发者工具中点击“元素”选项卡,您可以轻松地检查页面并一次看到输出,如下截图所示:

这个视图对于修复错误和调试问题非常有帮助。

Mozilla Firefox

如果您是 Mozilla Firefox 的粉丝,我们也可以使用 Firefox 来实现前面的结果。一旦 Ionic 应用程序启动,按下 Mac 上的Command + Option + I,或者在 Windows/Linux 上按下Ctrl + Shift + I,打开开发者工具(不是 Firebug,Firefox 的本机开发工具)。然后点击浏览器窗口旁边的停靠图标,如下截图所示:

现在我们可以拖动分界线,以实现与 Chrome 中看到的相同结果:

Ionic 项目结构

到目前为止,我们已经搭建了一个空白的 Ionic 应用程序并在浏览器中启动了它。现在,我们将浏览搭建好的项目结构。

如果我们在文本编辑器中打开chapter2 example1文件夹,我们应该在项目的根目录看到以下文件夹结构:

. 
├── config.xml 
├── hooks 
├── ionic.config.json 
├── node_modules 
├── package.json 
├── platforms 
├── plugins 
├── resources 
├── src 
├── tsconfig.json 
├── tslint.json 
├── www

以下是每个项目的快速解释:

  • src:这是所有开发发生的文件夹。应用程序源代码将放在这里。如果您从 Ionic 1 转到 Ionic 2,这是您会注意到的第一个变化。对我来说,这是文件夹结构的一个很好的升级,因为它将开发代码与部署代码分开。

  • hooks:这个文件夹包含了在执行特定的 Cordova 任务时执行的脚本。Cordova 任务可以是以下任何一种:after_platform_add(添加新平台后)、after_plugin_add(添加新插件后)、before_emulate(模拟开始前)、after_run(应用程序运行前)等。每个任务都放在以 Cordova 任务命名的文件夹内。

  • resources:这个文件夹包含了基于移动操作系统的应用程序图标和启动画面的各种版本。

  • www:这个文件夹包含了在src文件夹中编写的构建 Ionic 代码。这个文件夹中的所有代码都打算放在 WebView 中。

  • config.xml:这个文件包含了 Cordova 在将我们的 Ionic 应用程序转换为特定于平台的安装程序时所需的所有元信息。如果您打开config.xml,您将看到一堆描述我们项目的 XML 标签。我们将再次详细查看这个文件。

  • ionic.config.js:这个文件包含了构建任务所需的配置。

  • package.json:这个文件包含了项目级别的 node 依赖项。

  • tsconfig.json:这个文件包含了 TypeScript 的配置。

  • tslint.json:这个文件包含了 TS lint 规则。要了解更多关于这些规则的信息,请参考:palantir.github.io/tslint/rules/

config.xml 文件

config.xml文件是一个与平台无关的配置文件。如前所述,这个文件包含了 Cordova 在将www文件夹中的代码转换为特定于平台的安装程序时所需的所有信息。

config.xml文件的设置基于 W3C 的打包 Web 应用程序(小部件)规范(www.w3.org/TR/widgets/),并扩展为指定核心 Cordova API 功能、插件和特定于平台的设置。我们可以向该文件添加两种类型的配置。一种是全局的,即对所有设备通用,另一种是特定于平台的。

如果我们打开config.xml,我们会遇到的第一个标签是 XML 根标签。接下来,我们可以看到 widget 标签:

<widget id="app.example.one" version="0.0.1"  >

之前指定的id是我们应用程序的反向域名,我们在脚手架时提供的。其他规范是在 widget 标签内定义的其子级。子级标签包括应用程序名称(在设备上安装时显示在应用程序图标下方)、应用程序描述和作者详细信息。

它还包含了在将src文件夹中的代码转换为本机安装程序时需要遵守的配置。

内容标签定义了应用程序的起始页面。

访问标签定义了应用程序中允许加载的 URL。默认情况下,它会加载所有的 URL。

preference 标签设置了各种选项的名称值对。例如,DisallowOverscroll描述了当用户滚动文档的开头或结尾时是否应该有任何视觉反馈。

您可以在以下链接中阅读有关特定于平台的配置的更多信息:

平台特定配置和全局配置的重要性是一样的。您可以在docs.phonegap.com/en/edge/config_ref_index.md.html#The%20config.xml%20File了解更多关于全局配置的信息。

src 文件夹

正如前面提到的,该文件夹包括我们的 Ionic 应用程序,HTML、CSS 和 JS 代码。如果我们打开src文件夹,我们将找到以下文件结构:

. . 
├── app 
│   ├── app.component.ts 
│   ├── app.html 
│   ├── app.module.ts 
│   ├── app.scss 
│   ├── main.ts 
├── assets 
│   ├── icon 
├── declarations.d.ts 
├── index.html 
├── manifest.json 
├── pages 
│   ├── home 
├── service-worker.js 
├── theme 
    ├── variables.scss

让我们详细看看每一个:

  • app 文件夹:app 文件夹包括特定环境的初始化文件。该文件夹包括app.module.ts,其中定义了@NgModule模块。app.component.ts包括根组件。

  • assets 文件夹:该文件夹包括所有静态资产。

  • pages 文件夹:该文件夹包括我们将要创建的页面。在这个例子中,我们已经有一个名为home的示例页面。每个页面都是一个组件,其中包括业务逻辑-home.ts,标记-home.html和与组件相关的样式-home.scss

  • theme 文件夹:该文件夹包括variables.scss,覆盖它将改变 Ionic 组件的外观和感觉。

  • index.html:这是一切的起点。

这完成了我们对空白模板的介绍。在我们搭建下一个模板之前,让我们快速查看一下src/app/app.component.ts文件。

正如您所看到的,我们正在创建一个新的应用/根组件。@Component装饰器需要一个templatetemplateUrl属性来正确加载 Ionic 2 应用程序。作为模板的一部分,我们添加了ion-nav组件。

在类定义内部,我们声明了一个rootPage并将其分配给主页,并在构造函数内部,我们有平台准备好的回调,当平台准备就绪时将调用它。

这是一个非常简单和基本的 Ionic 应用程序。到目前为止,您一定已经在与 Web 相关的 Angular 代码上工作过。但是当您处理 Ionic 时,您将与与设备功能相关的脚本一起工作。Ionic 为我们提供了服务,以更有条理地实现这些功能。

搭建选项卡模板

为了更好地了解 Ionic CLI 和项目结构,我们还将搭建其他两个起始模板。首先我们将搭建选项卡模板。

使用cd命令,返回到chapter2文件夹并运行以下命令:

ionic start -a "Example 2" -i app.example.two example2 tabs --v2

选项卡项目被搭建在example2文件夹内。使用cd命令,进入example2文件夹并执行以下命令:

    ionic serve

我们应该看到使用 Ionic 构建的选项卡界面应用程序,如下面的屏幕截图所示:

选项卡位于页面底部。我们将在第三章,Ionic 组件和导航,以及第四章,Ionic 装饰器和服务中更多地讨论自定义。

如果您回到example2文件夹并分析项目结构,除了src/pages文件夹的内容外,其他都是一样的。

这一次,在 pages 文件夹中您将看到四个文件夹。tabs 文件夹包括选项卡定义,about、contact 和 home 文件夹包括每个选项卡的定义。

现在您可以很好地了解 Ionic 是如何与 Angular 集成的,以及所有组件是如何相辅相成的。当我们处理更多 Ionic 的部分时,这种结构将更加有意义。

搭建侧边菜单模板

现在我们将搭建最终的模板。使用cd命令,返回到chapter2文件夹并运行以下命令:

ionic start -a "Example 3" -i app.example.three example3 sidemenu --v2

执行脚手架项目,使用cd命令,进入example3文件夹并输入以下命令:

ionic serve

输出应该类似于以下截图:

您可以自行分析项目结构并查看区别。

您可以运行ionic start -lionic templates来查看可用模板的列表。您还可以使用ionic start task和列表中的模板名称来搭建应用程序。

摘要

在本章中,我们了解了移动混合架构的一些知识。我们还学习了混合应用程序的工作原理。我们看到了 Cordova 如何将 HTML、CSS 和 JS 代码拼接在一起,以在本地应用程序的 WebView 中执行。然后我们安装了开发 Ionic 应用程序所需的软件。我们使用 Ionic CLI 搭建了一个空白模板并分析了项目结构。随后,我们搭建了另外两个模板并观察了它们之间的区别。

您还可以参考 Ionic 幻灯片ionicframework.com/present-ionic/slides获取更多信息。

在下一章节Ionic 组件和导航中,我们将学习 Ionic 组件以及如何构建一个简单的两页应用程序并在它们之间进行导航。这将帮助我们使用 Ionic API 构建有趣的用户界面和多页面应用程序。

第三章:Ionic 组件和导航

到目前为止,我们已经了解了 Ionic 是什么,以及它在移动混合应用开发的大局中扮演的角色。我们还看到了如何搭建一个 Ionic 应用程序。

在本章中,我们将使用 Ionic 组件、Ionic 网格系统和 Ionic 中的导航。我们将查看 Ionic 的各种组件,使用这些组件可以构建提供出色用户体验的应用程序。

本章将涵盖以下主题:

  • Ionic 网格系统

  • Ionic 组件

  • Ionic 导航

核心组件

Ionic 是一个强大的移动 CSS 框架和 Angular 的结合。使用 Ionic,将任何想法推向市场所需的时间非常短。Ionic CSS 框架包含了构建应用程序所需的大多数组件。

为了测试可用组件,我们将搭建一个空白的起始模板,然后添加 Ionic 的可视组件。

在开始搭建之前,我们将创建一个名为chapter3的新文件夹,并在该文件夹中搭建本章的所有示例。

要搭建一个空白应用程序,请运行以下代码:

ionic start -a "Example 4" -i app.example.four example4 blank --v2 

Ionic 网格系统

要对布局进行精细控制,以便在页面上定位组件或以一致的方式将元素排列在一起,您需要一个网格系统,Ionic 提供了这样一个系统。

Ionic 网格系统的美妙之处在于它是基于 FlexBox 的。FlexBox——或 CSS 柔性盒布局模块——为优化的用户界面设计提供了一个盒模型。

您可以在以下链接了解更多关于 FlexBox 的信息:

www.w3.org/TR/css3-flexBox/

您可以在以下链接找到有关 FlexBox 的精彩教程:

css-tricks.com/snippets/css/a-guide-to-flexbox/

基于 FlexBox 的网格系统的优势在于,您不需要固定列网格。您可以在一行内定义尽可能多的列,并且它们将自动分配相等的宽度。这样,与任何其他基于 CSS 的网格系统不同,您不需要担心类名的总和是否等于网格系统中的总列数。

要了解网格系统的工作原理,请打开example4/src/pages/home文件夹中的home.html文件。删除ion-content指令内的所有内容,并添加以下代码:

<ion-row> 
        <ion-col>col-20%-auto</ion-col> 
        <ion-col>col-20%-auto</ion-col> 
        <ion-col>col-20%-auto</ion-col> 
        <ion-col>col-20%-auto</ion-col> 
        <ion-col>col-20%-auto</ion-col> 
</ion-row>

为了直观地看到区别,我们在src/pages/home文件夹中的home.scss中添加以下样式:

ion-col { 
    border: 1px solid red; 
}

上述样式不是使用网格系统所必需的;它只是为了显示布局中每个列的视觉分隔。

保存home.htmlhome.scss文件,并使用cd命令进入example4文件夹,然后运行以下命令:

ionic serve

然后您应该看到以下内容:

为了检查宽度是否会自动变化,我们将子 div 的数量减少到三个,如下所示:

<ion-row> 
        <ion-col>col-33%-auto</ion-col> 
        <ion-col>col-33%-auto</ion-col> 
        <ion-col>col-33%-auto</ion-col> 
</ion-row>

然后您应该看到以下内容:

无需麻烦,无需计算;您只需要添加要使用的 ion-col,它们将自动分配相等的宽度。

但这并不意味着您不能应用自定义宽度。您可以使用 Ionic 提供的宽度属性轻松实现这一点。

例如,假设在前面的三列情况下,您希望第一列跨越 50%,剩下的两列占据剩余的宽度;您只需要在第一个ion-col中添加一个名为width-50的属性,如下所示:

    <ion-row> 
        <ion-col width-50>col-50%-set</ion-col> 
        <ion-col>col-25%-auto</ion-col> 
        <ion-col>col-25%-auto</ion-col> 
   </ion-row>

然后您应该看到以下内容:

您可以参考以下表格,了解预定义宽度属性及其隐含宽度的列表:

属性名称 百分比宽度
width-10 10%
width-20 20%
width-25 25%
width-33 33.333%
width-34 33.333%
width-50 50%
width-66 66.666%
width-67 66.666%
width-75 75%
width-80 80%
width-90 90%

你还可以通过一定的百分比来偏移列。例如,将以下标记附加到我们当前的示例中:

<ion-row> 
        <ion-col offset-33>col-33%-offset</ion-col> 
        <ion-col>col-33%-auto</ion-col> 
 </ion-row>

然后你应该会看到以下内容:

第一个 div 偏移了 33%,剩下的 66%将在两个 div 之间分配。偏移属性所做的就是在 div 的左侧添加指定百分比的边距。

你可以参考以下表格,了解预定义类及其隐含的偏移宽度:

属性名称 百分比宽度
offset-10 10%
offset -20 20%
offset -25 25%
offset -33 33.333%
offset -34 33.333%
offset -50 50%
offset -66 66.666%
offset -67 66.666%
offset -75 75%
offset -80 80%
offset -90 90%

你还可以垂直对齐网格中的列。这是使用 FlexBox 网格系统的另一个优势。

添加以下代码:

<h4 text-center>Align Cols to <i>top</i></h4> 
    <ion-row top> 
        <ion-col> 
            <div>col</div> 
        </ion-col> 
        <ion-col> 
            <div>col</div> 
        </ion-col> 
        <ion-col> 
            <div>col</div> 
        </ion-col> 
        <ion-col> 
            <div> 
                This 
                <br>is a tall 
                <br> column 
            </div> 
        </ion-col> 
    </ion-row> 
    <h4 text-center>Align Cols to <i>center</i></h4> 
    <br> 
    <ion-row center> 
        <ion-col> 
            <div>col</div> 
        </ion-col> 
        <ion-col> 
            <div>col</div> 
        </ion-col> 
        <ion-col> 
            <div>col</div> 
        </ion-col> 
        <ion-col> 
            <div> 
                This 
                <br>is a tall 
                <br> column 
            </div> 
        </ion-col> 
    </ion-row> 
    <h4 text-center>Align Cols to <i>bottom</i></h4> 
    <ion-row bottom> 
        <ion-col> 
            <div>col</div> 
        </ion-col> 
        <ion-col> 
            <div>col</div> 
        </ion-col> 
        <ion-col> 
            <div>col</div> 
        </ion-col> 
        <ion-col> 
            <div> 
                This 
                <br>is a tall 
                <br> column 
            </div> 
        </ion-col> 
    </ion-row>

然后你应该会看到以下内容:

如果其中一个列很高,你可以在ion-row标记上添加 top、center 或 bottom 属性,事情就会如前面的图所示的那样落实到位。

有了这样一个简单而强大的网格系统,布局可能是无限的。

要了解更多关于 Ionic 网格系统的信息,你可以参考以下链接:ionicframework.com/docs/components/#grid

Ionic 组件

在本节中,我们将介绍一些 Ionic 组件。这些组件包括按钮、列表、卡片和表单。Ionic 组件会根据运行设备自动适应 iOS 主题,或者根据 Android 或 Windows 主题的 Material Design。当我们使用 Ionic 组件时,我们将在所有三个平台上看到输出。

要进一步进行,我们为按钮创建一个新项目。你可以cdchapter3文件夹,并运行以下命令:

ionic start -a "Example 5" -i app.example.five example5 blank --v2 

接下来,我们在实验室模式下为应用提供服务。使用cd命令导航到example5文件夹,并运行以下命令:

ionic serve --lab

这将在实验室模式下为 Ionic 应用提供服务,看起来会像这样:

通过这个视图,我们可以在所有三个平台上看到所有组件的输出。

按钮

Ionic 提供了不同的按钮变化,包括大小和样式。

src/pages/home/home.html中更新ion-content指令,使用以下代码,我们应该会看到不同的按钮变化:

<ion-content class="home" padding> 
    <button ion-button>Button</button> 
    <button ion-button color="light" outline>Light Outline</button> 
    <button ion-button color="secondary" clear>Secondary Clear</button> 
    <button ion-button color="danger" round>Danger Round</button> 
    <button ion-button block>Block Button</button> 
    <button ion-button color="secondary" full>Full Button</button> 
    <button ion-button color="danger" large>Large Danger</button> 
    <button ion-button dark> 
        Home 
        <ion-icon name="home"></ion-icon> 
    </button> 
</ion-content>

你注意到了ion-content指令上的填充属性吗?这将为ion-content指令添加16px的填充。如果你保存文件,你应该会看到这个:

前面的截图涵盖了基于默认 Ionic 颜色样本的所有按钮需求。

另外,你是否注意到按钮的外观在 iOS、Android 和 Windows 之间有所不同?我们将在第五章Ionic 和 SCSS中更多地讨论如何自定义这些组件。

有关按钮组件的更多信息,请参考:ionicframework.com/docs/api/components/button/Button

列表

ion-content section:
<ion-list> 
        <ion-item> 
            Light 
        </ion-item> 
        <ion-item> 
            Primary 
        </ion-item> 
        <ion-item> 
            Secondary 
        </ion-item> 
        <ion-item> 
            Danger 
        </ion-item> 
        <ion-item> 
            Dark 
        </ion-item> 
 </ion-list>

你应该会看到以下内容:

通过向ion-list指令添加一个名为no-lines的属性,线条将消失。如果你将前面的代码片段更新为以下内容:

<ion-list no-lines> 
        <ion-item> 
            Light 
        </ion-item> 
        <ion-item> 
            Primary 
        </ion-item> 
        <ion-item> 
            Secondary 
        </ion-item> 
        <ion-item> 
            Danger 
        </ion-item> 
        <ion-item> 
            Dark 
        </ion-item> 
    </ion-list>

你应该能够看到以下屏幕:

你还可以使用ion-item-group将列表项分组在一起。其代码如下:

<ion-list> 
    <ion-item-group> 
        <ion-item-divider light>A</ion-item-divider> 
        <ion-item>Apple</ion-item> 
        <ion-item>Apricots</ion-item> 
        <ion-item>Avocado</ion-item> 
        <ion-item-divider light>B</ion-item-divider> 
        <ion-item>Bananas</ion-item> 
        <ion-item>Blueberries</ion-item> 
        <ion-item>Blackberries</ion-item> 
    </ion-item-group>  
</ion-list>

为此,ion-list将被替换为ion-item-group,如前面的代码片段所示。你应该会看到以下屏幕:

Ionic 列表的新添加是滑动列表。在这种类型的列表中,每个项目都可以向左滑动以显示新选项。

这段代码的片段如下所示:

<ion-list> 
        <ion-item-sliding> 
            <ion-item> 
                <ion-avatar item-left> 
                    <img src="img/~text?
                     txtsize=23&txt=80%C3%9780&w=80&h=80"> 
                </ion-avatar> 
                <h2>Indiana Jones</h2> 
                <p>Played by Harrison Ford in Raiders of the Lost Ark
                </p> 
            </ion-item> 
            <ion-item-options> 
                <button ion-button color="light"> 
                    <ion-icon name="ios-more"></ion-icon> 
                    More 
                </button> 
                <button ion-button color="primary"> 
                    <ion-icon name="text"></ion-icon> 
                    Text 
                </button> 
                <button ion-button color="secondary"> 
                    <ion-icon name="call"></ion-icon> 
                    Call 
                </button> 
            </ion-item-options> 
        </ion-item-sliding> 
        <ion-item-sliding> 
            <ion-item> 
                <ion-avatar item-left> 
                    <img src="img/~text?
                     txtsize=23&txt=80%C3%9780&w=80&h=80"> 
                </ion-avatar> 
                <h2>James Bond</h2> 
                <p>Played by Sean Connery in Dr. No</p> 
            </ion-item> 
            <ion-item-options> 
                <button ion-button color="light"> 
                    <ion-icon name="ios-more"></ion-icon> 
                    More 
                </button> 
                <button ion-button color="primary"> 
                    <ion-icon name="text"></ion-icon> 
                    Text 
                </button> 
                <button ion-button color="secondary"> 
                    <ion-icon name="call"></ion-icon> 
                    Call 
                </button> 
            </ion-item-options> 
        </ion-item-sliding> 
</ion-list>

前面代码的输出如下所示:

有关列表组件的更多信息,您可以参考以下链接:ionicframework.com/docs/components/#lists

卡片

卡片是在移动设备上展示内容的最佳设计模式之一。对于显示用户个性化内容的任何页面或应用程序,卡片都是最佳选择。世界正在向卡片展示内容的方式发展,包括在某些情况下也在桌面上。例如 Twitter (dev.twitter.com/cards/overview)和 Google Now。

因此,您也可以将该设计模式简单地移植到您的应用程序中。您需要做的就是设计适合卡片的个性化内容,并将其放入ion-card组件中:

<ion-card> 
 <ion-card-header> 
      Card Header 
 </ion-card-header> 
<ion-card-content> 
            Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dignissimos magni itaque numquam distinctio pariatur voluptas sint, id inventore nulla vitae. Veritatis animi eos cupiditate. Labore, amet debitis maxime velit assumenda. 
</ion-card-content> 
</ion-card>

ion-card-header directive and the output would look as follows:

您可以通过向卡片添加图像来为卡片增添创意:

<ion-card> 
        <img src="img/~text?
         txtsize=72&txt=600%C3%97390&w=600&h=390" /> 
        <ion-card-content> 
            <h2 class="card-title"> 
        quas quae sunt 
      </h2> 
            <p> 
                Lorem ipsum dolor sit amet, 
                consectetur adipisicing elit. Magni nihil 
                hic vel fugit dignissimos ad natus eaque! 
                Perspiciatis beatae quis doloremque soluta 
                enim ratione laboriosam. Dolore illum, 
                quas quae sunt. 
            </p> 
        </ion-card-content> 
        <ion-row no-padding> 
            <ion-col width-33> 
                <button ion-button clear small color="danger"> 
                    <ion-icon name='star'></ion-icon> 
                    Dolore 
                </button> 
            </ion-col> 
            <ion-col width-33> 
                <button ion-button clear small color="danger"> 
                    <ion-icon name='musical-notes'></ion-icon> 
                    Perspi 
                </button> 
            </ion-col> 
            <ion-col width-33> 
                <button ion-button clear small color="danger"> 
                    <ion-icon name='share-alt'></ion-icon> 
                    Magni 
                </button> 
            </ion-col> 
        </ion-row> 
</ion-card>

这将如下所示:

您还可以使用卡片来显示地图:

    <ion-card> 
        <div style="position: relative"> 
            <img src="img/staticmap?
             center=Malaysia&size=640x400&style=element:
             labels|visibility:off&style=
             element:geometry.stroke|visibility:off&style=
             feature:landscape|element:
             geometry|saturation:-100&style=feature:
             water|saturation:-100|invert_lig
             htness:true&key=
             AIzaSyA4rAT0fdTZLNkJ5o0uaAwZ89vVPQpr_Kc"> 
            <ion-fab bottom right edge> 
                <button ion-fab mini> 
                    <ion-icon name='pin'></ion-icon> 
                </button> 
            </ion-fab> 
        </div> 
        <ion-item> 
            <ion-icon subtle large item-left name='map'></ion-icon> 
            <h2>Malaysia</h2> 
            <p>Truely Asia!!</p> 
        </ion-item> 
    </ion-card>

你应该能够看到以下屏幕:

有了ion-card的强大功能,您可以将应用程序提升到一个新水平!

Ionic 图标

Ionic 拥有自己的 700 多个字体图标。添加图标的最简单方法如下:

<ion-icon name="heart"></ion-icon>

您可以从这里找到图标的名称:ionicons.com

您可以使用is-active属性将图标标记为活动或非活动。活动图标通常是完整和粗的,而非活动图标是轮廓和细的:

<ion-icon name="beer" isActive="true"></ion-icon> 
<ion-icon name="beer" isActive="false"></ion-icon>

图标也可以根据平台进行设置;以下片段显示了如何设置:

<ion-icon ios="logo-apple" md="logo-android"></ion-icon>

您也可以通过首先创建一个分配给变量的属性,然后在构造函数中填充该变量,以编程方式设置图标名称。HTML 片段如下所示:

<ion-icon [name]="myIcon"></ion-icon>

TypeScript 代码(在home.ts中)如下所示:

import { Component } from '@angular/core';

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

  myIcon: String;
  iconNames: Array<String> = ['home', 'map', 'pin', 'heart', 'star'];

  constructor(public navCtrl: NavController) {
    this.myIcon = this.iconNames[Math.floor(Math.random() * 
    this.iconNames.length)];
  }
}

前面片段的整合输出如下:

模态框

在本节中,我们将看一下 Ionic 中的模态框以及如何实现它们。要使用此示例,我们需要搭建一个新项目:

ionic start -a "Example 6" -i app.example.six example6 blank --v2

cd进入example6文件夹并运行ionic serve --lab,您应该看到空白模板的主页。

要使用模态框,我们需要首先创建一个要显示为模态框的组件。

example6文件夹内运行以下命令:

ionic generate component helloModal

注意:我们将在本章的后面部分讨论子生成器。

注意:如果您使用的是最新的 Ionic CLI,您将看到一个名为hello-modal.module.ts的文件与hello-modal.htmlhello-modal.scsshello-modal.ts一起生成。要了解有关hello-modal.module.ts的更多信息,请参考第十一章,Ionic 3

生成组件后,我们需要将其添加到@NgModule中。打开src/app/app.module.ts并添加import语句:

import { HelloModalComponent } 
from '../components/hello-modal/hello-modal';

注意:生成的组件可能具有HelloModal而不是HelloModalComponent的类名。如果是这种情况,请相应更新。

接下来,将HelloModalComponent添加到declarationsentryComponents中,如下所示:

@NgModule({ 
  declarations: [ 
    MyApp, 
    HomePage, 
    HelloModalComponent 
  ], 
  imports: [ 
    IonicModule.forRoot(MyApp) 
  ], 
  bootstrap: [IonicApp], 
  entryComponents: [ 
    MyApp, 
    HomePage, 
    HelloModalComponent 
  ], 
  providers: [ 
    StatusBar, 
    SplashScreen, 
    {provide: ErrorHandler, useClass: IonicErrorHandler} 
  ] 
})

现在已经完成,我们开始配置组件。打开src/pages/home/home.ts并更新如下:

import { Component } from '@angular/core'; 
import { ModalController } from 'ionic-angular'; 
import { HelloModalComponent } from '../../components/hello-modal/hello-modal'; 

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

   constructor(public modalCtrl: ModalController) { } 

   show() { 
      let modal = this.modalCtrl.create(HelloModalComponent); 
      modal.present(); 
      modal.onDidDismiss((data) => { 
         console.log(data); 
      }); 
   } 
}

如你所见,对于使用modal组件,我们有一个ModalController。使用ModalController实例的create(),我们可以注册一个模态框。然后,使用present(),我们显示模态框。

更新src/pages/home/home.html以显示一个按钮。点击该按钮将呈现模态框:

<ion-header> 
  <ion-navbar> 
    <ion-title> 
      My Modal App 
    </ion-title> 
  </ion-navbar> 
</ion-header> 

<ion-content padding> 
  <button ion-button color="primary" (click)="show()">Show Modal</button> 
</ion-content>

接下来,我们更新HelloModalComponent。打开src/components/hello-modal/hello-modal.ts并更新如下:

import { Component } from '@angular/core'; 
import { ViewController } from 'ionic-angular'; 

@Component({ 
  selector: 'hello-modal', 
  templateUrl: 'hello-modal.html' 
}) 
export class HelloModalComponent { 

  constructor(public viewCtrl: ViewController) { } 

  close() { 
    this.viewCtrl.dismiss({'random' : 'data'}); 
  } 
}

在这里,我们使用ViewController的实例来管理弹出窗口。最后,对于弹出窗口的内容,打开src/components/hello-modal/hello-modal.html并更新如下:

<ion-content padding> 
    <h2>I'm a modal!</h2> 
    <button ion-button color="danger" (click)="close()">Close</button> 
</ion-content>

有了这个,我们已经添加了所有需要的代码。保存所有文件并运行ionic serve -lab以查看输出。

输出应如下所示:

分段

Segment 是 Ionic 的另一个新功能。这个组件用于控制单选按钮的选择。我们将搭建另一个应用程序来使用这个示例。从chapter3文件夹内,运行以下命令:

ionic start -a "Example 7" -i app.example.seven example7 blank --v2 

cd进入example7文件夹,运行ionic serve --lab,你应该会看到空模板的主页。

ion-content directive in the src/pages/home/home.html file:
    <ion-segment [(ngModel)]="food" color="primary"> 
        <ion-segment-button value="pizza"> 
            Pizza 
        </ion-segment-button> 
        <ion-segment-button value="burger"> 
            Burger 
        </ion-segment-button> 
    </ion-segment> 
    <div [ngSwitch]="food"> 
        <ion-list *ngSwitchCase="'pizza'"> 
            <ion-item> 
                <ion-thumbnail item-left> 
                    <img src="img/~text?
                     txtsize=23&txt=80%C3%9780&w=80&h=80"> 
                </ion-thumbnail> 
                <h2>Pizza 1</h2> 
            </ion-item> 
            <ion-item> 
                <ion-thumbnail item-left> 
                    <img src="img/~text?
                     txtsize=23&txt=80%C3%9780&w=80&h=80"> 
                </ion-thumbnail> 
                <h2>Pizza 2</h2> 
            </ion-item> 
        </ion-list> 
        <ion-list *ngSwitchCase="'burger'"> 
            <ion-item> 
                <ion-thumbnail item-left> 
                    <img src="img/~text?
                     txtsize=23&txt=80%C3%9780&w=80&h=80"> 
                </ion-thumbnail> 
                <h2>Burger 1</h2> 
            </ion-item> 
            <ion-item> 
                <ion-thumbnail item-left> 
                    <img src="img/~text?
                     txtsize=23&txt=80%C3%9780&w=80&h=80"> 
                </ion-thumbnail> 
                <h2>Burger 2</h2> 
            </ion-item> 
        </ion-list> 
    </div>

我们在src/pages/home/home.ts文件中将 food 属性初始化为pizza,如下所示:


import { Component } from '@angular/core'; 
import { NavController } from 'ionic-angular'; 

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

   constructor(public navCtrl: NavController) { 
      this.food = 'pizza'; 
   } 
}

输出应该如下所示:

Ionic 导航

在本节中,我们将看看 Ionic 导航。我们将搭建一个空模板,然后添加更多页面,看看如何在它们之间导航。

Ionic 3 引入了@IonicPage装饰器,用于简化和改进导航,围绕原生移动体验。请查看第十一章,Ionic 3

基本导航

要开始,我们需要搭建一个新项目。运行以下命令:

ionic start -a "Example 8" -i app.example.eight example8 blank --v2

使用ionic serve命令运行 Ionic 应用,你应该会看到空模板的主页。

Ionic 中的导航不需要 URL;相反,页面是从导航控制器的页面堆栈中推送和弹出的。与基于浏览器的导航相比,这种方法非常符合在原生移动应用中实现导航的方式。但是,你可以使用 URL 进行页面深度链接,但这并不定义导航。

要了解基本导航,我们打开src/app/app.html文件,应该会找到以下模板:

<ion-nav [root]="rootPage"></ion-nav>

ion-navNavController的子类,其目的是与导航页面堆栈一起工作。为了让ion-nav正常工作,我们必须将根页面设置为最初加载的页面,其中根页面是任何@component

所以如果我们看app.component.ts,它指向一个名为 rootPage 的局部变量,并且设置为 HomePage。

现在,在src/pages/home/home.html中,我们会看到顶部有一个部分,如下所示:

  <ion-navbar> 
    <ion-title> 
      Ionic Blank 
    </ion-title> 
  </ion-navbar>

这是动态导航栏。

src/pages/home/home.ts内,我们可以按如下方式访问NavController

import { Component } from '@angular/core'; 
import { NavController } from 'ionic-angular'; 

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

  } 
}

现在我们可以访问导航属性。

Ionic CLI 子生成器

全新的 Ionic CLI v2 现在充满了子生成器,可以帮助搭建页面、组件、提供者等。要查看可用子生成器的列表,可以运行以下命令:

ionic generate --list 

你会看到以下内容:

现在,我们将使用前面的子生成器,在example8项目内生成两个页面。运行以下命令:

ionic generate page about

还要运行以下命令:

ionic generate page contact

app/pages文件夹内,你应该会看到两个新文件夹,about 和 contact 文件夹,它们有自己的htmltsscss文件,以及module.ts文件。

类名为About而不是AboutPage。如果是这样,请相应地更新前面的内容。

在我们继续之前,我们需要按如下方式将AboutPageContactPage添加到src/app/app.module.ts中:

import { NgModule, ErrorHandler } from '@angular/core'; 
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular'; 
import { MyApp } from './app.component'; 
import { HomePage } from '../pages/home/home'; 
import { AboutPage } from '../pages/about/about'; 
import { ContactPage } from '../pages/contact/contact'; 

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

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

多页面导航

现在我们有了三个页面,我们将看看如何在它们之间实现导航。从主页,用户应该能够转到关于和联系页面,从关于页面转到联系和主页,最后从联系页面转到主页和关于页面。

首先,我们按如下方式更新home.html

<ion-header> 
    <ion-navbar> 
        <ion-title> 
            Home Page 
        </ion-title> 
    </ion-navbar> 
</ion-header> 
<ion-content padding> 
    <ion-card> 
        <ion-card-header> 
            Home Page 
        </ion-card-header> 
        <ion-card-content> 
            <button ion-button (click)="goTo('about')">About</button> 
            <button ion-button color="danger"  
             (click)="goTo('contact')">Contact</button> 
            <button ion-button color="light" 
             (click)="back()">Back</button> 
        </ion-card-content> 
    </ion-card> 
</ion-content>

接下来,我们按如下方式更新home.ts


import { Component } from '@angular/core'; 
import { NavController } from 'ionic-angular'; 

import { AboutPage } from '../about/about'; 
import { ContactPage } from '../contact/contact'; 

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

   goTo(page) { 
      if (page === 'about') { 
         this.navCtrl.push(AboutPage); 
      } else if (page === 'contact') { 
         this.navCtrl.push(ContactPage); 
      } 
   } 

   back() { 
      if (this.navCtrl.length() >= 2) { 
         this.navCtrl.pop(); 
      } 
   } 
}

你注意到goToback函数了吗?这就是我们从一个页面导航到另一个页面的方式。

接下来,我们将按如下方式更新about.html

<ion-header> 
    <ion-navbar> 
        <ion-title> 
            About Page 
        </ion-title> 
    </ion-navbar> 
</ion-header> 
<ion-content padding> 
    <ion-card> 
        <ion-card-header> 
            About Page 
        </ion-card-header> 
        <ion-card-content> 
            <button ion-button (click)="goTo('home')">Home</button> 
            <button ion-button color="danger" 
             (click)="goTo('contact')">Contact</button> 
            <button ion-button color="light" 
             (click)="back()">Back</button> 
        </ion-card-content> 
    </ion-card> 
</ion-content>

about.ts如下:

import { Component } from '@angular/core'; 
import { NavController } from 'ionic-angular'; 

import { HomePage } from '../home/home'; 
import { ContactPage } from '../contact/contact'; 

@Component({ 
   selector: 'page-home', 
   templateUrl: 'home.html' 
}) 
export class AboutPage { 
   constructor(private navCtrl: NavController) { } 

   goTo(page) { 
      if (page === 'home') { 
         this.navCtrl.push(HomePage); 
      } else if (page === 'contact') { 
         this.navCtrl.push(ContactPage); 
      } 
   } 

   back() { 
      if (this.navCtrl.length() >= 2) { 
         this.navCtrl.pop(); 
      } 
   } 
}

最后,contact.html

<ion-header> 
    <ion-navbar> 
        <ion-title> 
            Contact Page 
        </ion-title> 
    </ion-navbar> 
</ion-header> 
<ion-content padding> 
    <ion-card> 
        <ion-card-header> 
            Contact Page 
        </ion-card-header> 
        <ion-card-content> 
            <button ion-button (click)="goTo('home')">Home</button> 
            <button ion-button color="danger" 
             (click)="goTo('about')">About</button> 
            <button ion-button color="light" 
             (click)="back()">Back</button> 
        </ion-card-content> 
    </ion-card> 
</ion-content>

以及contact.ts如下:


import { Component } from '@angular/core'; 
import { NavController } from 'ionic-angular'; 

import { HomePage } from '../home/home'; 
import { AboutPage } from '../about/about'; 

@Component({ 
   selector: 'page-home', 
   templateUrl: 'home.html' 
}) 
export class ContactPage { 
   constructor(private navCtrl: NavController) { } 

   goTo(page) { 
      if (page === 'home') { 
         this.navCtrl.push(HomePage); 
      } else if (page === 'about') { 
         this.navCtrl.push(AboutPage); 
      } 
   } 

   back() { 
      if (this.navCtrl.length() >= 2) { 
         this.navCtrl.pop(); 
      } 
   } 
}

如果我们保存所有文件并返回浏览器,我们应该会看到以下内容:

当我们点击 About 按钮时,我们应该会看到以下屏幕:

正如我们所看到的,返回按钮会自动添加到导航栏中。现在,当我们点击返回按钮时,我们将返回到主页。如果你注意到了返回功能,我们添加了一个条件来检查堆栈中是否有多个视图以弹出视图。如果只有一个视图,它将被移除,用户将看到一个黑屏,如下所示:

为了避免应用程序中的黑屏死机,我们添加了这个条件。

现在我们了解了 Ionic 应用程序中的导航,你可以回到标签模板和侧边菜单模板,并查看src文件夹以开始。

另外,请查看第十一章,Ionic 3,了解更多关于@IonicPage修饰符以及深度链接的信息。

摘要

在本章中,我们已经了解了 Ionic 网格系统和一些主要的 Ionic 组件,并且看到了如何使用它们。我们介绍了按钮、列表、卡片、图标和段落。接下来,我们将看到如何使用导航组件以及如何在页面之间导航。

在下一章中,我们将使用 Ionic 修饰符和服务,并且我们将看看 Ionic 提供的修饰符和服务。

第四章:Ionic 装饰器和服务

在上一章中,我们通过了一些 Ionic 组件,使用这些组件可以轻松构建时尚的移动混合应用程序。在这一章中,我们将使用 Ionic 2 的装饰器和服务。整个 Ionic 2 生态系统分为两部分:组件和服务 API。组件包括按钮、卡片和列表,正如我们在上一章中看到的,服务 API 包括平台、configNavControllerStorage等等。

在这一章中,我们将看一下以下主题:

  • Ionic 模块

  • 组件装饰器

  • 配置服务

  • 平台服务

  • 存储 API

装饰器

在我们开始使用 Ionic 内置装饰器之前,我们将快速了解装饰器是什么,以及它们如何让我们的生活变得更容易。

简单来说,装饰器是一个接受类并扩展其行为而不实际修改它的函数。

例如,如果我们有一个人类,并且我们想要向类中添加关于这个人的更多信息,比如年龄和性别,我们可以很容易地做到这一点。

以下是我们如何在 TypeScript 中编写自己的装饰器的示例:

@MoreInfo({ 
    age: 5, 
    gender: 'male' 
}) 
class Person { 
    constructor(private firstName, private lastName) {} 
}

MoreInfo装饰器看起来会像这样:

function MoreInfo(config) { 
    return function (target) { 
        Object.defineProperty(target.prototype, 'age', {value: () => config.age}); 
        Object.defineProperty(target.prototype, 'gender', {value: () => config.gender}); 
    } 
}

同样,Ionic 还提供了两个装饰器:

  • Ionic 模块或NgModule装饰器

  • 组件装饰器

Ionic 模块

Ionic 模块或NgModule装饰器引导 Ionic 应用程序。如果我们打开任何现有的 Ionic 项目并查看src/app/app.module.ts文件,我们会看到以下内容:

import { NgModule, ErrorHandler } from '@angular/core'; 
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular'; 
import { MyApp } from './app.component'; 
import { HomePage } from '../pages/home/home'; 
import { AboutPage } from '../pages/about/about'; 
import { ContactPage } from '../pages/contact/contact'; 

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

这是我们引导 Ionic 应用程序的地方。这个应用程序也可以通过在IonicModule上使用forRoot来配置。forRoot同时负责提供和配置服务。

IonicModule上实现forRoot的一个示例看起来像这样:

import { IonicApp, IonicModule } from 'ionic-angular'; 
import { MyApp } from './app.component'; 

@NgModule({ 
    declarations: [MyApp], 
    imports: [ 
        IonicModule.forRoot(MyApp, { 
            backButtonText: 'Go Back', 
            iconMode: 'ios', 
            modalEnter: 'modal-slide-in', 
            modalLeave: 'modal-slide-out', 
            tabsPlacement: 'bottom', 
            pageTransition: 'ios' 
        }, {}) 
    ], 
    bootstrap: [IonicApp], 
    entryComponents: [MyApp], 
    providers: [] 
})

平台特定的配置也可以被传递,如下所示:

import { IonicApp, IonicModule } from 'ionic-angular'; 
import { MyApp } from './app.component'; 

@NgModule({ 
    declarations: [MyApp], 
    imports: [ 
        IonicModule.forRoot(MyApp, { 
            backButtonText: 'Go Back', 
            platforms: { 
                ios: { 
                    iconMode: 'ios', 
                    modalEnter: 'modal-slide-in', 
                    modalLeave: 'modal-slide-out', 
                    tabbarPlacement: 'bottom', 
                    pageTransition: 'ios-transition', 
                }, 
                android: { 
                    iconMode: 'md', 
                    modalEnter: 'modal-md-slide-in', 
                    modalLeave: 'modal-md-slide-out', 
                    tabbarPlacement: 'top', 
                    pageTransition: 'md-transition', 
                } 
            } 

        }, {}) 
    ], 
    bootstrap: [IonicApp], 
    entryComponents: [MyApp], 
    providers: [] 
})

您可以在ionicframework.com/docs/v2/api/IonicModule/了解更多关于 Ionic 模块的信息,关于配置请访问:ionicframework.com/docs/v2/api/config/Config/,关于NgModule请访问angular.io/docs/ts/latest/guide/ngmodule.html

组件装饰器

Component装饰器标记一个类为 Angular 组件,并收集组件配置元数据。一个简单的组件装饰器看起来像这样:

import { Component } from '@angular/core'; 
import { Platform } from 'ionic-angular'; 
import { StatusBar, Splashscreen } from 'ionic-native'; 

import { HomePage } from '../pages/home/home'; 

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

  constructor(platform: Platform) { 
    platform.ready().then(() => { 
    StatusBar.styleDefault(); 
       Splashscreen.hide(); 
    }); 
  } 
}

组件包括所有 Ionic 和 Angular 核心组件和指令,因此我们不需要显式声明指令属性。只有子/父组件上的依赖属性需要显式指定。

要了解更多关于Component装饰器的信息,请参考angular.io/docs/ts/latest/api/core/index/Component-decorator.html

导航

在上一章中,我们看到了在两个页面之间进行导航的基本实现。在本节中,我们将更深入地研究相同的内容。

首先,我们将脚手架一个空白的 Ionic 应用程序。创建一个名为chapter4的新文件夹,在该文件夹内打开一个新的命令提示符/终端,并运行以下命令:

ionic start -a "Example 9" -i app.example.nine example9 blank --v2

一旦应用程序被脚手架化,cd进入example9文件夹。如果我们导航到example9/src/app/app.component.ts,我们应该看到由名为MyApp的类定义的 App 组件。如果我们导航到相应的模板example9/src/app/app.html,我们应该看到ion-nav组件。

ion-nav组件接受一个名为 root 的输入属性。root 属性指示哪个组件将充当根组件/根页面。在这个例子中,我们已经从我们的MyApp类(example9/src/app/app.component.ts)中指定了 Home Page 作为root

现在我们将生成一个名为 about 的新页面,使用 Ionic CLI 的 generate 命令。运行以下命令:

ionic generate page about

这个命令将在src/pages文件夹内创建一个新的组件。

如果我们查看example9/src/pages/home/example9/src/pages/about/的内容,我们应该会看到两个独立的组件。

在我们开始将这两个页面连接在一起之前,我们首先需要使用@NgModule注册关于页面。打开example9/src/app/app.module.ts并按照以下方式更新它:

import { NgModule, ErrorHandler } from '@angular/core'; 
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular'; 
import { MyApp } from './app.component'; 
import { HomePage } from '../pages/home/home'; 
import { AboutPage } from '../pages/about/about'; 

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

接下来,我们将在主页上添加一个按钮,当我们点击它时,我们将显示关于页面。按照以下方式更新example9/src/pages/home/home.html

<ion-header> 
  <ion-navbar> 
    <ion-title> 
      Home Page 
    </ion-title> 
  </ion-navbar> 
</ion-header> 

<ion-content padding> 
   <button ion-button color="secondary" (click)="openAbout()">Go To About</button> 
</ion-content>

接下来,我们将添加页面之间导航的逻辑。按照以下方式更新example9/src/pages/home/home.ts

import { Component } from '@angular/core'; 
import { NavController } from 'ionic-angular'; 
import { AboutPage } from '../about/about'; 

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

  constructor(public navCtrl: NavController) {} 

  openAbout(){ 
    this.navCtrl.push(AboutPage); 
  } 
}

使用this.navCtrl.push(AboutPage);,我们从主页跳转到关于页面。

如果我们保存文件并执行ionic serve,我们应该会看到带有按钮的主页。当我们点击按钮时,我们应该会看到关于页面:

现在,如果我们想要导航回去,我们可以使用自动生成的返回按钮,或者我们可以在关于页面上创建一个按钮返回。为了做到这一点,请按照以下方式更新example9/src/pages/about/about.html

<ion-header> 
  <ion-navbar> 
    <ion-title>About Page</ion-title> 
  </ion-navbar> 
</ion-header> 

<ion-content padding> 
   <button ion-button color="light" (click)="goBack()">Back</button> 
</ion-content>

并按照以下方式更新example9/src/pages/about/about.ts

import { Component } from '@angular/core'; 
import { NavController } from 'ionic-angular'; 

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

  constructor(public navCtrl: NavController) {} 

  goBack(){ 
    this.navCtrl.pop(); 
  } 
}

请注意this.navCtrl.pop();--这是我们从视图中弹出页面的方法。

如果我们保存所有文件并返回浏览器,然后从主页导航到关于,我们应该会看到一个返回按钮。点击它将会带我们回到主页。

这是一个简单的例子,说明了我们如何将两个页面连接在一起。

除此之外,我们还有页面事件,指示页面的各个阶段。为了更好地理解这一点,我们将按照以下方式更新example9/src/pages/about/about.ts

import { Component } from '@angular/core'; 
import { NavController } from 'ionic-angular'; 

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

  constructor(public navCtrl: NavController) { } 

  goBack() { 
    this.navCtrl.pop(); 
  } 

  ionViewDidLoad() { 
    console.log("About page: ionViewDidLoad Fired"); 
  } 

  ionViewWillEnter() { 
    console.log("About page: ionViewWillEnter Fired"); 
  } 

  ionViewDidEnter() { 
    console.log("About page: ionViewDidEnter Fired"); 
  } 

  ionViewWillLeave() { 
    console.log("About page: ionViewWillLeave Fired"); 
  } 

  ionViewDidLeave() { 
    console.log("About page: ionViewDidLeave Fired"); 
  } 

  ionViewWillUnload() { 
    console.log("About page: ionViewWillUnload Fired"); 
  } 

  ionViewDidUnload() { 
    console.log("About page: ionViewDidUnload Fired"); 
  } 
}

保存所有文件,导航到浏览器,从主页导航到关于,然后返回,我们应该会看到以下内容:

基于此,我们可以挂接到各种事件,并在需要时采取相应的行动。

在页面之间传递数据

到目前为止,我们已经看到了如何从一个页面移动到另一个页面。现在,使用NavParams,我们将从一个页面传递数据到另一个页面。

在相同的example9项目中,我们将添加这个功能。在主页上,我们将呈现一个文本框,供用户输入数据。一旦用户输入数据并点击转到关于,我们将获取textbox的值,并将其传递到关于页面,并在关于页面中打印我们在主页上捕获的文本。

要开始,我们将按照以下方式更新example9/src/pages/home/home.html

<ion-header> 
    <ion-navbar> 
        <ion-title> 
            Home Page 
        </ion-title> 
    </ion-navbar> 
</ion-header> 
<ion-content padding> 
    <ion-list> 
        <ion-item> 
            <ion-label color="primary">Enter</ion-label> 
            <ion-input placeholder="Something..." #text></ion-input> 
        </ion-item> 
    </ion-list> 
    <button ion-button color="secondary" (click)="openAbout(text.value)">Go To About</button> 
</ion-content>

请注意,我们已经更新了openAbout方法以获取文本值。接下来,我们将更新example9/src/pages/home/home.ts

import { Component } from '@angular/core'; 
import { NavController } from 'ionic-angular'; 
import { AboutPage } from '../about/about'; 

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

  constructor(public navCtrl: NavController) {} 

  openAbout(text){ 
    text = text || 'Nothing was entered'; 

    this.navCtrl.push(AboutPage, { 
      data : text 
    }); 
  } 
}

请注意我们传递给navCtrl的 push 方法的第二个参数。这是我们如何从主页传递数据。现在我们将更新example9/src/pages/about/about.ts以捕获数据:

import { Component } from '@angular/core'; 
import { NavController, NavParams } from 'ionic-angular'; 

@Component({ 
  selector: 'page-about', 
  templateUrl: 'about.html' 
}) 
export class AboutPage { 
  text: string; 

  constructor(public navCtrl: NavController, public navParams: NavParams) {  
    this.text = navParams.get('data'); 
  } 

  goBack() { 
    this.navCtrl.pop(); 
  } 

  /// SNIPP :: Page events... 
}

为了捕获数据,我们需要从ionic-angular中导入NavParams。并且使用navParams.get(data);,我们在构造函数中获取从主页传递过来的数据。

最后,为了在关于页面中显示数据,请按照以下方式更新example9/src/pages/about/about.html

<ion-header> 
    <ion-navbar> 
        <ion-title>About Page</ion-title> 
    </ion-navbar> 
</ion-header> 
<ion-content padding> 
    <label>Text Entered : {{text}}</label> 
    <br> 
    <button ion-button color="light" (click)="goBack()">Back</button> 
</ion-content>

保存所有文件并返回浏览器,我们应该能够看到以下内容:

现在我们知道如何将两个页面连接在一起并在它们之间传递数据。

我们可以使用@IonicPage装饰器实现导航和延迟加载。您可以在第十一章中找到更多关于此的信息,Ionic 3

配置服务

该服务允许您配置和设置特定于应用程序的首选项。

为了在各种组件中跨平台或在同一平台内定制应用程序的外观和感觉,我们使用配置服务。

为了更好地理解这项服务,我们将搭建一个新的应用程序并与之一起工作。运行以下命令:

ionic start -a "Example 10" -i app.example.ten example10 tabs --v2

然后运行ionic serve --lab

这将在实验室视图中运行选项卡应用程序,我们可以在其中同时看到 Android iOS 和 Windows 应用程序。

我们还可以使用以下 URL 在三个平台视图中查看 Ionic 应用程序:

iOS:localhost:8100/?ionicplatform=ios Android:localhost:8100/?ionicplatform=android Windows:localhost:8100/?ionicplatform=windows

我们应该看到类似于这样的东西:

配置设置在@NgModule上。如果我们打开example10/src/app/app.module.ts,我们应该找到NgModule装饰器,在其中我们可以找到IonicModule.forRoot(MyApp)

简单的配置看起来像这样:

//... snipp 
imports: [ 
    IonicModule.forRoot(MyApp, { 
        mode: 'md' 
    }) 
  ], 
//.. snipp

这将使外观和感觉默认为材料设计,而不考虑平台。我们应该能够看到以下内容:

您还可以像这样设置其他配置值:

//.. snipp 
imports: [ 
    IonicModule.forRoot(MyApp, { 
      backButtonText: 'Go Back', 
      iconMode: 'ios', 
      modalEnter: 'modal-slide-in', 
      modalLeave: 'modal-slide-out', 
      tabsPlacement: 'bottom', 
      pageTransition: 'ios', 
    }) 
  ], 
//... snipp

前面的值相当自明。

配置中的属性可以在应用程序级别、平台级别和组件级别进行覆盖。

例如,您可以在应用程序级别以及平台级别覆盖tabberPlacement属性,如下所示:

//..snipp 
imports: [ 
    IonicModule.forRoot(MyApp, { 
      tabsPlacement: 'bottom', // bottom for all platforms 
      platforms: { 
        ios: { 
          tabsPlacement: 'top', // top only for iOS 
        } 
      } 
    }) 
  ], 
//...snipp

我们将看到以下内容:

我们也可以在组件级别进行覆盖。更新example10/src/pages/tabs/tabs.html如下:

<ion-tabs tabsPlacement="top"> 
  <ion-tab [root]="tab1Root" tabTitle="Home" tabIcon="home"></ion-tab> 
  <ion-tab [root]="tab2Root" tabTitle="About" tabIcon="information-circle"></ion-tab> 
  <ion-tab [root]="tab3Root" tabTitle="Contact" tabIcon="contacts"></ion-tab> 
</ion-tabs>

我们应该看到以下内容:

为了快速测试,我们还可以在 URL 中设置配置属性,而不定义任何覆盖。例如,要测试将选项卡放在顶部时的外观,我们可以转到此 URL:localhost:8100/?ionicTabsPlacement=top

我们还可以在配置中设置自定义属性,并在以后提取它们。例如,我们可以设置以下属性:

config.set('ios', 'themePref', 'dark');

然后我们可以使用以下方法获取值:

config.get('themePref');

我们可以从ionic-angular导入config,例如import {Config} from 'ionic-angular';,然后在构造函数中初始化configconstructor(private config : Config) { //**// }

平台服务

平台服务返回有关当前平台的可用信息。Ionic 的新版平台服务提供了更多信息,帮助我们根据设备类型定制应用程序。

为了更好地了解平台服务,我们将创建一个空白应用程序。运行以下命令:

ionic start -a "Example 11" -i app.example.eleven example11 blank --v2

然后运行ionic serve启动空白应用程序。

现在我们将在example11/src/pages/home/home.ts中添加对 Platform 类的引用。更新home.ts如下:

import { Component } from '@angular/core'; 
import { Platform } from 'ionic-angular'; 

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

现在我们将开始使用Platform类的各种功能。

我们要查看的第一个是userAgent字符串。要访问userAgent,我们可以在平台上执行userAgent()

更新example11/src/pages/home/home.html内容部分如下:

<ion-header> 
    <ion-navbar> 
        <ion-title> 
            Ionic Blank 
        </ion-title> 
    </ion-navbar> 
</ion-header> 
<ion-content padding> 
    <ion-card> 
        <ion-card-header> 
            Platform : User Agent 
        </ion-card-header> 
        <ion-card-content> 
            {{platform.userAgent()}} 
        </ion-card-content> 
    </ion-card> 
</ion-content>

我们应该看到以下内容:

接下来,我们将找出应用程序正在运行的平台;为此,我们将更新home.html中的ion-content内容如下:

<ion-card> 
    <ion-card-header> 
      Platform : platformName 
    </ion-card-header> 
    <ion-card-content> 
      <ion-list> 
        <ion-item> 
          android : {{platform.is('android')}} 
        </ion-item> 
        <ion-item> 
          cordova : {{platform.is('cordova')}} 
        </ion-item> 
        <ion-item> 
          core : {{platform.is('core')}} 
        </ion-item> 
        <ion-item> 
          ios : {{platform.is('ios')}} 
        </ion-item> 
        <ion-item> 
          ipad : {{platform.is('ipad')}} 
        </ion-item> 
        <ion-item> 
          iphone : {{platform.is('iphone')}} 
        </ion-item> 
        <ion-item> 
          mobile : {{platform.is('mobile')}} 
        </ion-item> 
        <ion-item> 
          mobileweb : {{platform.is('mobileweb')}} 
        </ion-item> 
        <ion-item> 
          phablet : {{platform.is('phablet')}} 
        </ion-item> 
        <ion-item> 
          tablet : {{platform.is('tablet')}} 
        </ion-item> 
        <ion-item> 
          windows : {{platform.is('windows')}} 
        </ion-item> 
      </ion-list> 
    </ion-card-content> 
  </ion-card>

当浏览器刷新时,我们应该看到以下内容:

正如我们从屏幕截图中看到的,当在浏览器中运行时,前面的平台名称是这些值。

现在,让我们添加浏览器平台并查看是否有任何更改。运行以下命令:

ionic platform add browser 

然后运行:

ionic run browser

您应该能够在浏览器中看到 Ionic 应用程序启动,并且现在输出应该如下所示:

如果我们仔细观察,我们可以看到在前面的屏幕截图中,cordova现在设置为true

使用前面的平台名称,我们可以轻松定制应用程序并调整用户体验。

要了解有关平台服务的更多信息,请参阅ionicframework.com/docs/api/platform/Platform/

存储服务

在本节中,我们将研究存储服务。Ionic 的 Storage 类帮助我们与应用程序在原生容器中运行时可用的各种存储选项进行交互。

引用 Ionic 文档:

Storage 是一种存储键/值对和 JSON 对象的简单方法。Storage 在底层使用各种存储引擎,根据平台选择最佳的存储引擎。

在本机应用上下文中运行时,Storage 将优先使用 SQLite,因为它是最稳定和广泛使用的基于文件的数据库之一,并且避免了一些像 localstorage 和 IndexedDB 这样的问题,比如操作系统决定在低磁盘空间情况下清除这些数据。

在 Web 或作为渐进式 Web 应用运行时,Storage 将尝试使用 IndexedDB、WebSQL 和 localstorage,按照这个顺序。

现在,要开始使用 Storage 类,我们将创建一个新的应用。运行以下命令:

ionic start -a "Example 12" -i app.example.twelve example12 blank --v2 

然后运行ionic serve在浏览器中启动它。

为了了解如何使用 Storage,我们将构建一个简单的用户管理应用。在这个应用中,我们可以添加用户,将数据持久化存储,然后稍后删除它。这个应用的主要目的是探索 Storage 类。

最终的应用将看起来像下面这样:

在开始使用Storage类之前,我们需要将其添加到我们的Ionic项目中。运行以下命令:

npm install --save @ionic/storage

接下来,我们需要将其添加为提供者。按照以下方式更新example12/src/app/app.module.ts

import { BrowserModule } from '@angular/platform-browser'; 
import { ErrorHandler, NgModule } from '@angular/core'; 
import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular'; 
import { SplashScreen } from '@ionic-native/splash-screen'; 
import { StatusBar } from '@ionic-native/status-bar'; 

import { MyApp } from './app.component'; 
import { HomePage } from '../pages/home/home'; 
import { IonicStorageModule } from '@ionic/storage'; 

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

接下来,我们将构建界面。打开example12/src/pages/home/home.html。首先我们将更新头部如下:

<ion-header> 
    <ion-navbar> 
        <ion-title> 
            Manage Users 
        </ion-title> 
    </ion-navbar> 
</ion-header>

接下来,在内容部分,我们将创建两个部分,一个用于用户输入姓名和年龄的表单,另一个用于显示用户列表的部分:

<ion-content padding> 
    <div> 
        <ion-list> 
            <ion-item> 
                <ion-label fixed>Name</ion-label> 
                <ion-input type="text" placeholder="Enter Name" #name>
                </ion-input> 
            </ion-item> 
            <ion-item> 
                <ion-label fixed>Age</ion-label> 
                <ion-input type="number" placeholder="Enter Age" #age> 
                </ion-input> 
            </ion-item> 
        </ion-list> 
        <button ion-button full color="primary" (click)="addUser(name, 
        age)" [disabled]="!name.value || !age.value">Create 
        User</button> 
    </div> 
    <div *ngIf="users.length > 0"> 
        <h3 style="text-align: center;" padding>Users</h3> 
        <ion-card *ngFor="let user of users"> 
            <ion-card-content> 
                <ion-label>Name : {{user.name}}</ion-label> 
                <ion-label>Age : {{user.age}}</ion-label> 
                <button ion-button color="danger" 
                (click)="removeUser(user)">Delete User</button> 
            </ion-card-content> 
        </ion-card> 
    </div> 
</ion-content>

接下来,我们将开始处理逻辑。按照以下方式更新example12/src/pages/home/home.ts

import { Component } from '@angular/core'; 
import { NavController } from 'ionic-angular'; 
import { Storage } from '@ionic/storage'; 

@Component({ 
  selector: 'page-home', 
  templateUrl: 'home.html' 
}) 
export class HomePage { 
  users: any[] = []; 

  constructor(private navCtrl: NavController, private storage: Storage) { 
    // get all the users from storage on load 
    this.getUsers(); 
  } 

  getUsers() { 
    this.storage.ready().then(() => { 
      this.storage.forEach((v, k, i) => { 
        if (k.indexOf('user-') === 0) { 
          this.users.push(v); 
        } 
      }); 
    }); 
  } 

  addUser(name, age) { 
    this.storage.ready().then(() => { 
      let user = { 
        id: this.genRandomId(), 
        name: name.value, 
        age: age.value 
      }; 
      // save it to the storage 
      this.storage.set('user-' + user.id, user); 
      // update the inmemory variable to refresh the UI 
      this.users.push(user); 
      // reset the form 
      name.value = ''; 
      age.value = ''; 
    }); 
  } 

  removeUser(user) { 
    this.storage.ready().then(() => { 
      // remove from storage 
      this.storage.remove('user-' + user.id); 
      // update the inmemory variable to refresh the UI 
      this.users.splice(this.users.indexOf(user), 1); 
    }); 
  } 

  genRandomId() { 
    return Math.floor(Math.random() * 9999); // up to 4 digits random number 
  } 

}

在上面的代码中,首先我们从@ionic/storage中导入了Storage。接下来,在构造函数中实例化了相同的内容。

我们创建了一个名为users的类变量,用于在内存中存储我们创建的所有用户。在构造函数内部,我们调用getUsers()来在加载时从存储中获取用户。我们创建了两个函数,addUser()removeUser(),用于添加用户和删除用户。

由于存储是一个键值存储,我们使用用户的 ID 创建存储的键。例如,如果用户的 ID 是 1,我们将键创建为user-1。这样,我们知道存储中属于我们应用的所有键都以user开头,以防其他实体在同一个应用中使用 Storage。

我们使用genRandomId()来生成一个 1 到 9999 之间的随机数。

如果我们保存所有文件,返回浏览器,并打开控制台,我们应该看到类似以下的内容:

请注意控制台中的消息。这条消息告诉我们数据将被存储在 asynStorage 中。因此,在 Chrome 中,它将是 IndexedDB。

因此,在 Chrome 中,如果我们在开发工具中点击应用程序选项卡并导航到 IndexedDB,我们应该看到类似以下的内容:

现在,让我们使用表单添加一个用户。更新后的屏幕和存储应该如下所示:

现在,点击删除后,我们应该看到存储已清除,并且 UI 更新后没有任何用户。

因此,使用存储,我们可以在 Ionic 应用中轻松开始处理数据持久性,而不必担心底层实现。

如果需要,我们可以覆盖IonicStorageModule.forRoot()如下:

IonicStorageModule.forRoot({
  name: 'appDB',
  driverOrder: ['indexeddb', 'sqlite', 'websql']
})

您可以在这里找到更多配置和属性:ionicframework.com/docs/storage/

通过这样,我们完成了 Ionic 中 Storage 的概述。

摘要

在本章中,我们已经介绍了 Ionic 的两个主要装饰器。然后我们介绍了配置和平台服务,并看到了如何根据平台和配置自定义应用程序。之后,我们介绍了 Ionic 中的存储 API。请参考第十一章,Ionic 3,了解全新的IonicPage指令和IonicPage模块。

在下一章中,我们将学习如何为 Ionic 应用创建主题。

第五章:Ionic 和 SCSS

在本章中,我们将介绍使用 Ionic 进行主题设置。Ionic 中的主题设置简单且易于实现。Ionic 团队在简化和模块化 Ionic 中的主题设置方面付出了很大的努力。简而言之,Ionic 中的主题设置发生在组件级别,以及平台级别(iOS、Android 和 WP)。Ionic 使用 SCSS 来处理主题设置。在本章中,我们将介绍以下主题:

  • Sass 与 SCSS

  • 使用 SCSS 变量

  • 平台级别和页面/组件级别的覆盖

什么是 Sass?

引用自 Sass 文档:

“Sass 是 CSS 的扩展,为基本语言增添了力量和优雅。”

它允许我们使用变量、嵌套规则、mixin、内联导入等,所有这些都是完全兼容 CSS 的语法。Sass 有助于保持大型样式表的良好组织,并快速启动小型样式表。

简单来说,Sass 使 CSS 可编程。但是,本章的标题是 SCSS;为什么我们要谈论 Sass 呢?嗯,Sass 和 SCSS 基本上是相同的 CSS 预处理器,每个都有自己的编写预 CSS 语法的方式。

SCSS 是作为另一个名为 HAML(haml.info/)的预处理器的一部分而开发的,由 Ruby 开发人员,因此它继承了很多来自 Ruby 的语法风格,例如缩进、无大括号和无分号。

一个示例的 Sass 文件看起来像这样:

// app.sass 

brand-primary= blue 

.container 
    color= !brand-primary 
    margin= 0px auto 
    padding= 20px 

=border-radius(!radius) 
    -webkit-border-radius= !radius 
    -moz-border-radius= !radius 
    border-radius= !radius 

* 
    +border-radius(0px) 

通过 Sass 编译器运行,它将返回以下代码:

.container { 
  color: blue; 
  margin: 0px auto; 
  padding: 20px; 
} 

* { 
  -webkit-border-radius: 0px; 
  -moz-border-radius: 0px; 
  border-radius: 0px; 
}

好老的 CSS。但是你有没有注意到brand-primary作为一个变量,在容器类内替换它的值?以及border-radius作为一个函数(也称为 mixin),在调用时生成所需的 CSS 规则?是的,这是 CSS 编程中缺失的一部分。你可以尝试前面的转换:sasstocss.appspot.com/,看看 Sass 是如何编译成 CSS 的。

习惯于基于大括号的编码语言的人会觉得这种编写代码的方式有点困难。所以,SCSS 应运而生。

Sass 代表Syntactically Awesome Style Sheets,SCSS 代表Sassy CSS。因此,SCSS 基本上与 Sass 相同,除了类似于 CSS 的语法。前面的 Sass 代码,如果用 SCSS 编写,会变成这样:

$brand-primary: blue; 

.container{ 
    color: !brand-primary; 
    margin: 0px auto; 
    padding: 20px; 
} 

@mixin border-radius($radius) { 
    -webkit-border-radius: $radius; 
    -moz-border-radius: $radius; 
    border-radius: $radius; 
} 

* { 
    @include border-radius(5px); 
}

这看起来更接近 CSS 本身,对吧?而且它很有表现力。Ionic 使用 SCSS 来为其组件设置样式。

如果你想了解更多关于 SCSS 与 Sass 的信息,你可以查看:thesassway.com/editorial/sass-vs-scss-which-syntax-is-better

现在我们对 SCSS 和 Sass 是什么以及如何使用它们有了基本的了解,我们将利用它们在我们的 Ionic 应用程序中来维护和设置主题。

Ionic 和 SCSS

默认情况下,Ionic 已经集成了 SCSS。与早期版本不同,在那个版本中,人们必须在项目中设置 SCSS,在 Ionic 2 中,主题设置变得更加模块化和简单。主题设置可以发生在两个级别:

  • 在平台级别

  • 在页面/组件级别

应用级别的主题设置几乎总是我们所需要的。我们会根据我们的品牌更改应用程序的颜色,由于 Ionic 使用了 SCSS 映射,颜色直接被组件继承。此外,我们可以根据需要添加、重命名和删除颜色。映射中唯一需要的颜色是主要颜色。如果颜色因模式而异,iOS、MD 和 WP 颜色可以进一步自定义。

如果我们希望保持我们的样式与那些页面/组件隔离并特定于它们,页面/组件级别的主题设置非常有帮助。这是应用程序开发的基于组件的方法的最大优势之一。我们可以保持我们的组件模块化和可管理,同时防止样式和功能从一个组件泄漏到另一个组件,除非有意为之。

为了掌握 Ionic 中的主题设置,我们将搭建一个新的选项卡应用程序并设置相同的主题。如果需要,创建一个名为chapter5的新文件夹,然后打开一个新的命令提示符/终端。运行以下命令:

ionic start -a "Example 13" -i app.example.thirteen example13 tabs 
--v2

一旦应用程序被脚手架搭建,运行ionic serve在浏览器中查看应用程序。我们要处理的第一件事是颜色。打开example13/src/theme/variables.scss,我们应该会看到一个名为$colors的变量映射。

为了快速测试颜色方案,将$colors映射中的主要变量的值从#387ef5更改为red。我们应该会看到以下内容:

如前所述,主要是唯一的强制值。

颜色映射也可以扩展以添加我们自己的颜色。例如,在example13/src/pages/home/home.html上,让我们添加一个带有属性名称purple的按钮,看起来会像这样:

<ion-content padding> 
    <button ion-button color="purple">A Purple Button</button> 
</ion-content>

$colors映射中,添加一个新的键值:purple: #663399。完整的映射看起来像这样:

$colors: ( 
  primary:    red, 
  secondary:  #32db64, 
  danger:     #f53d3d, 
  light:      #f4f4f4, 
  dark:       #222, 
  purple:     #663399 
);

现在,如果我们返回到页面,我们应该会看到以下内容:

确实很简单地向我们的应用程序添加新颜色。

我们可以通过添加基础和对比属性来进一步定制主题颜色。基础将是元素的背景,对比将是文本颜色。

为了测试上述功能,打开example13/src/pages/about/about.html,并按照下面的代码添加一个浮动操作按钮:

<ion-content padding> 
  <button ion-fab color="different">FAB</button> 
</ion-content>

color=different to the FAB. We will be using this variable name to apply styles.

我们更新的$colors映射将如下所示:

$colors: ( 
  primary:    red, 
  secondary:  #32db64, 
  danger:     #f53d3d, 
  light:      #f4f4f4, 
  dark:       #222, 
  purple:     #663399, 
  different: ( 
    base: #4CAF50, 
    contrast: #F44336 
  ) 
);

注意:这将为所有不同的 Ionic 组件生成样式。如果它们不是根组件的一部分,请不要将 SCSS 变量放在映射中。

保存所有文件后导航到关于选项卡时,我们应该会看到以下内容:

主题设置很简单吧?

页面级别覆盖

我们可以通过在两个不同页面中的同一组件上应用不同的样式,将相同的主题应用到下一个级别。例如,我们将使标签在关于页面和联系页面中看起来不同。这是我们将如何实现它的方式。

example13/src/pages/about/about.html中,我们将在ion-content部分内添加一个新的标签,如下面的代码所示:

<ion-content padding> 
  <button ion-fab color="different">FAB</button> 
  <label>This is a label that looks different from the one on Contact Page</label> 
</ion-content>

我们将在example13/src/pages/about/about.scss中添加所需的样式,如下面的代码所示:

page-about { 
    label { 
        border: 2px solid #FF5722; 
        background: #FF5722; 
    } 
}

同样,我们将在example13/src/pages/contact/contact.html中的ion-content部分内添加另一个标签,如下面的代码所示:

<ion-content> 
    <label>This is a label that looks different from the one on About Page</label> 
</ion-content>

我们将在example13/src/pages/contact/contact.scss中添加所需的样式,如下面的代码所示:

page-contact { 
    label { 
        border: 2px solid #009688; 
        background: #009688; 
        margin: 20px; 
        margin-top: 100px; 
        display: block; 
    } 
}

现在,如果我们保存所有文件并返回到浏览器中的关于页面,我们应该会看到以下内容:

联系页面将如下所示:

正如我们从上图中看到的,我们正在使用页面级样式来区分这两个组件。上面的截图是一个简单的例子,说明了我们如何在不同页面的同一组件中拥有多种样式。

平台级别覆盖

既然我们已经看到了如何在页面级别应用样式,让我们看看 Ionic 主题是如何简化在平台级别管理样式的。当在多个具有自己独特样式的设备上查看同一应用程序时,平台级样式是适用的。

在使用 Ionic 时,我们定义模式,其中模式是应用程序运行的平台。默认情况下,Ionic 会在ion-app元素上添加与模式相同的类名。例如,如果我们在 Android 上查看应用程序,body 将具有名为md的类,其中md代表material design

为了快速检查这一点,我们将打开http://localhost:8100/?ionicplatform=ios,然后在开发者工具中检查 body 元素。我们应该会看到ion-app元素带有一个名为ios的类,以及其他类:

如果我们打开http://localhost:8100/?ionicplatform=android,我们应该会看到以下内容:

如果我们打开http://localhost:8100/?ionicplatform=windows,我们应该会看到以下内容:

截至今天,Ionic 有三种模式:

平台 模式 描述
iOS ios 对所有组件应用 iOS 样式
Android md 对所有组件应用 Material Design 样式
Windows wp 对所有组件应用 Windows 样式
Core md 如果我们不在上述设备中的任何一个上,应用将默认获得 Material Design 样式

更多信息请参阅:ionicframework.com/docs/theming/platform-specific-styles/

我们将在example13/src/theme/variables.scss文件的注释提供的部分中定义特定于平台的样式。

为了理解特定于平台的样式,我们将为navbar应用不同的背景颜色并更改文本颜色。

打开example13/src/theme/variables.scss并在注释中说App Material Design Variables的部分下添加以下样式:

// App Material Design Variables 
// --------------------------------------------------

// Material Design only Sass variables can go here 
.md{ 
  ion-navbar .toolbar-background { 
      background: #FF5722; 
  } 

  ion-navbar .toolbar-title { 
      color: #fff; 
  } 
}

现在,当我们保存文件并导航到http://localhost:8100/?ionicplatform=android,我们应该看到以下内容:

请注意.md类,其中嵌套了样式。这就是使样式特定于平台的原因。

类似地,我们更新App iOS Variables部分:

// App iOS Variables 
// -------------------------------------------------- 
// iOS only Sass variables can go here 
.ios{ 
  ion-navbar .toolbar-background { 
      background: #2196F3; 
  } 

  ion-navbar .toolbar-title { 
      color: #fff; 
  } 
}

然后我们应该看到以下内容:

最后,对于 Windows,我们将根据以下代码更新App Windows Variables部分:

// App Windows Variables 
// -------------------------------------------------- 
// Windows only Sass variables can go here 
.wp{ 
  ion-navbar .toolbar-background { 
      background: #9C27B0; 
  } 

  ion-navbar .toolbar-title { 
      color: #fff; 
  } 
}

我们应该看到以下内容:

我们已经在第四章中看到了如何使用config属性将应用的模式更改为mdioswp

我们也可以动态设置平台并应用样式。

为了理解这一点,我们将使用徽章组件。只有在 Windows 平台上,徽章组件才不会有任何边框半径,但我们希望使用动态属性来覆盖这种行为。

ion-content section:
<ion-item> 
        <ion-icon name="logo-dropbox" item-left></ion-icon> 
        Files 
        <ion-badge item-right [attr.round-badge]="isWindows ? '' : null">175</ion-badge> 
    </ion-item>

如果我们注意到在ion-badge上,我们有一个条件属性[attr.round-badge]="isWindows ? '' : null"。如果平台是 Windows,我们将添加一个名为round-badge的新属性,并根据以下代码更新example13/src/pages/contact/contact.ts

import { Component } from '@angular/core'; 
import { Platform } from 'ionic-angular'; 

@Component({ 
  selector: 'page-contact', 
  templateUrl: 'contact.html' 
}) 
export class ContactPage { 
  isWindows: Boolean; 

  constructor(public platform: Platform) { 
    this.isWindows = platform.is('windows'); 
  } 
}

我们已经在构造函数中定义了isWindows的值。现在,如果我们保存所有文件并导航到http://localhost:8100/?ionicplatform=windows,我们应该看到以下内容:

如果我们检查徽章,我们应该看到添加了属性round-badge

我们可以导航到其他平台并验证相同的内容。

如果我们观察,徽章容器的边框有0px的边框半径。现在我们将在example13/src/theme/variables.scssApp Windows Variables部分中添加所需的覆盖。

代码片段如下所示:

.wp{ 
  // snipp 

  ion-badge[round-badge]{ 
    border-radius: 12px; 
  } 
}

现在,即使对于 Windows 平台,我们也可以看到border-radius被应用:

这是我们可以实现特定于平台的覆盖的另一种方式。

组件级别的覆盖

到目前为止,我们所见到的自定义大多是在页面和平台级别上。如果我们想要自定义 Ionic 提供的组件以匹配我们品牌的外观和感觉呢?

这也可以很容易地实现,这要归功于 Ionic 团队,他们已经在暴露变量名称以自定义属性的方面走了额外的一英里。

如果我们导航到ionicframework.com/docs/theming/overriding-ionic-variables/,我们将看到一个可过滤的表格,我们可以在其中找到可以覆盖的特定于组件的变量:

主题一个示例组件

为了快速检查这一点,我们将在当前应用的主页上实现覆盖加载栏。当用户登陆到这个标签页时,我们将以编程方式触发加载弹出窗口,并根据平台的不同,我们将自定义组件的外观和感觉,以展示组件可以根据我们的意愿进行自定义。

根据以下代码更新example13/src/pages/home/home.ts

import { Component } from '@angular/core'; 
import { LoadingController } from 'ionic-angular'; 

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

  constructor(public loadingCtrl: LoadingController) { 
    this.presentLoading(); 
  } 

  presentLoading() { 
    let loader = this.loadingCtrl.create({ 
      content: "Please wait...", 
      duration: 3000 
    }); 
    loader.present(); 
  } 
}

我们定义了一个名为presentLoading的函数,并在构造函数中调用它。这将在页面加载时显示加载条。

如果我们保存此页面并导航到三个不同的平台,我们将看到特定于该特定平台的样式。在这个例子中,我们将使所有的加载条看起来(几乎)一样,不管平台如何。我们将通过搞乱SCSS变量来实现相同的效果。

如果我们导航到ionicframework.com/docs/theming/overriding-ionic-variables/并过滤loading-ios,我们将看到一堆与加载弹出样式相关的 SCSS 变量。同样,如果我们搜索loading-md,我们将找到与 Android 相关的 SCSS 变量。最后,如果我们搜索loading-wp,我们会找到 Windows 平台的 SCSS 变量。

我们将使用前面的变量名并自定义外观和感觉。打开example13/src/theme/variables.scss。在定义了@import 'ionic.globals';之后,在定义颜色映射之前,我们将添加组件级别的覆盖。如果你看的是被注释的 SCSS 文件,你会看到一个名为Shared Variables的部分。这是我们添加变量覆盖的地方。

我们取了一些 SCSS 变量,并修改了它们的属性,如下所示的代码:

// Overriding Loading Popup for iOS  
// >> Start 
$loading-ios-background: #2196F3; 
$loading-ios-border-radius: 0px; 
$loading-ios-text-color: #fff; 
$loading-ios-spinner-color: #eee; 
// >> End 

// Overriding Loading Popup for Android  
// >> Start 
$loading-md-background: #2196F3; 
$loading-md-border-radius: 0px; 
$loading-md-text-color: #fff; 
$loading-md-spinner-color: #eee; 
// >> End 

// Overriding Loading Popup for Windows  
// >> Start 
$loading-wp-background: #2196F3; 
$loading-wp-border-radius: 0px; 
$loading-wp-text-color: #fff; 
$loading-wp-spinner-color: #eee; 
// >> End

现在,如果我们导航到http://localhost:8100/?ionicplatform=ios,我们应该会看到以下内容:

如果我们导航到http://localhost:8100/?ionicplatform=android,我们应该会看到以下内容:

最后,如果我们导航到http://localhost:8100/?ionicplatform=windows,我们应该会看到以下内容:

我们也可以添加自定义 CSS,使它们看起来都一样。

通过这样,我们完成了对 Ionic 应用在平台级别和页面/组件级别进行主题设置的概述。

摘要

在本章中,我们已经看到了如何为 Ionic 应用设置主题。我们还看到了如何可以轻松地在平台级别和页面/组件级别实现样式。

在下一章中,我们将看一下 Ionic Native。Ionic Native 对于 Ionic 1 来说就像 ngCordova 一样。我们将深入探讨如何将设备功能与 Ionic 应用集成。

第六章:Ionic Native

在本章中,我们将研究如何将设备特定功能(如网络、电池状态、相机等)集成到 Ionic 应用程序中。为了开始探索这一点,我们将首先研究 Cordova 插件,然后使用 Ionic Native。

在本章中,我们将看一下:

  • 设置特定于平台的 SDK

  • 使用 Cordova 插件 API

  • 使用 Ionic Native

  • 测试一些 Ionic Native 插件

设置特定于平台的 SDK

在我们开始与设备特定功能交互之前,我们需要在本地机器上设置该设备操作系统的 SDK。官方上,Ionic 支持 iOS、Android 和 Windows 手机平台。尽管如此,Ionic 可以在任何可以运行 HTML、CSS 和 JavaScript 的设备上使用。

以下是如何在本地机器上设置移动 SDK 的链接。不幸的是,如果没有设置,我们无法继续本章节(和书籍)。让我们看一下以下链接:

注意:对于其他支持的操作系统,您可以查看cordova.apache.org/docs/en/6.x/guide/overview/

在本书中,我们只会使用 Android 和 iOS。您也可以为其他移动平台采用类似的方法。在我们继续之前,我们需要确保设置已经完成,并且按预期工作。

Android 设置

确保已安装 SDK 并且 Android 工具在您的路径中:

  • 在计算机的任何位置的命令提示符/终端中运行:android。这将启动 Android SDK 管理器。确保您已安装最新版本的 Android,或者您正在针对安装特定版本。

  • 运行以下命令:

      android avd

  • 这将启动 Android 虚拟设备管理器。确保至少设置了一个 AVD。如果还没有这样做,您可以通过单击“创建”按钮轻松完成。您可以按照以下选项填写选项:

iOS 设置

确保您已安装 Xcode 和所需工具,并且已全局安装ios-simios-deploy

npm install -g ios-sim
npm install -g ios-deploy

iOS 设置只能在苹果设备上完成。Windows 开发人员无法从 Windows 设备部署 iOS 应用程序,因为需要 Xcode。

测试设置

让我们看看如何测试 Android 和 iOS 的设置。

测试 Android

为了测试设置,我们将创建一个新的 Ionic 应用程序,并使用 Android 和 iOS 模拟器进行模拟。我们将首先创建一个选项卡应用程序。创建一个名为chapter6的文件夹,并打开一个新的命令提示符/终端。运行以下命令:

ionic start -a "Example 14" -i app.example.fourteen example14 tabs --v2

要在 Android 模拟器上模拟应用程序,首先需要为此项目添加 Android 平台支持,然后模拟它:

添加 Android 平台,请运行以下命令:

ionic platform add android

完成后,请运行以下命令:

ionic emulate android

一段时间后,您将看到模拟器启动,并且应用程序将在模拟器内部部署和执行。如果您已经使用原生 Android 应用程序工作过,您就知道 Android 模拟器有多慢。如果您没有,它非常慢。Android 模拟器的替代方案是 Genymotion (www.genymotion.com)。Ionic 也与 Genymotion 很好地集成在一起。

Genymotion 有两种版本,一种是免费的,另一种是商业使用的。免费版本功能有限,只能用于个人使用。

您可以从以下网址下载 Genymotion 的副本:www.genymotion.com/#!/store

安装 Genymotion 后,请使用您喜欢的 Android SDK 创建一个新的虚拟设备。我的配置如下:

接下来,我们启动模拟器并让其在后台运行。现在 Genymotion 正在运行,我们需要告诉 Ionic 使用 Genymotion 而不是 Android 模拟器来模拟应用程序。为此,我们使用以下命令:

ionic run android

而不是这个:ionic emulate android

这将部署应用程序到 Genymotion 模拟器,您可以立即看到应用程序,而不像使用 Android 模拟器那样需要等待。

确保在运行应用程序之前,Genymotion 在后台运行。

如果 Genymotion 对您来说有点大,您可以简单地将 Android 手机连接到计算机并运行以下命令:

ionic run android

这将部署应用程序到实际设备。

要设置 Android USB 调试,请参考:developer.android.com/studio/run/device.html

Genymotion 的早期截图来自个人版,因为我没有许可证。在开发阶段,我通常与我的 Android 手机一起使用 iOS 模拟器。一旦整个开发完成,我会从设备农场购买设备时间,并在目标设备上进行测试。

如果在连接 Android 手机到计算机时遇到问题,请检查您是否能够在命令提示符/终端中运行adb device并在此处看到您的设备。您可以在developer.android.com/studio/command-line/adb.html找到有关Android 调试桥ADB)的更多信息。

测试 iOS

要测试 iOS,我们将首先添加 iOS 平台支持,就像我们为 Android 做的那样,然后模拟它。

运行以下命令:

ionic platform add ios

然后运行:ionic emulate ios

您应该看到默认的模拟器启动,最后,应用程序将出现在模拟器中,如下面的截图所示:

要部署到苹果设备,您可以运行以下命令:

ionic run ios

确保在继续之前能够模拟/运行应用程序。

使用 Cordova 插件入门

根据 Cordova 文档:

“插件是一种注入代码的包,允许应用程序呈现的 Cordova WebView 与其运行的本机平台进行通信。插件提供对设备和平台功能的访问,这些功能通常对基于 Web 的应用程序不可用。所有主要的 Cordova API 功能都是作为插件实现的,还有许多其他可用的插件,可以启用诸如条形码扫描仪、NFC 通信或定制日历界面等功能…”

换句话说,Cordova 插件是访问设备特定功能的窗口。Cordova 团队已经构建了需要的插件,以便几乎可以与所有设备特定功能一起使用。还有社区贡献的插件,可以提供围绕设备特定功能的定制包装。

您可以在这里搜索现有的插件:cordova.apache.org/plugins/

在本章的过程中,我们将探索一些插件。由于我们专注于 Ionic 特定的开发,我们将使用 Ionic CLI 添加插件。在幕后,Ionic CLI 调用 Cordova CLI 来执行必要的操作。

Ionic 插件 API

在处理插件时,我们将使用四个主要命令。

添加插件

此 CLI 命令用于向项目添加新插件:

ionic plugin add org.apache.cordova.camera

您也可以使用这个:

ionic plugin add cordova-plugin-camera

删除插件

此 CLI 命令用于从项目中删除插件:

ionic plugin rm org.apache.cordova.camera

您也可以使用这个:

ionic plugin rm cordova-plugin-camera

列出已添加的插件

此 CLI 命令用于列出项目中的所有插件,例如:

ionic plugin ls

搜索插件

此 CLI 命令用于从命令行搜索插件,例如:

ionic plugin search scanner barcode

Ionic Native

现在我们已经了解了如何使用 Cordova 插件,我们将创建一个新项目并与我们的 Ionic 应用程序集成 Cordova 插件。

Ionic 为我们提供了一个简单的包装器,以 TypeScript 的方式处理 Cordova 插件。在所有插件都采用 ES6/TS 方法之前,我们需要一种方法在我们的 Ionic 应用程序中使用这些插件。

进入 Ionic Native。Ionic Native 是当今 ES5 Cordova 插件的 ES6/TypeScript 实现,因此您可以导入所需的插件并在 TypeScript 中使用它们。Ionic 团队在以 TypeScript 绑定的形式向我们提供插件方面做得非常好。

Ionic Native 测试驱动

为了测试,我们将创建一个新项目并执行以下命令:

  1. 运行以下命令:
      ionic start -a "Example 15" -i app.example.fifteen 
      example15 blank --v2

cdexample15文件夹中。

  1. 让我们搜索电池状态插件并将其添加到我们的项目中。运行以下命令:
      ionic plugin search battery status

  1. 这将启动默认浏览器并将您导航到:cordova.apache.org/plugins/?q=battery%20status。根据你找到的插件名称,你可以将该插件添加到项目中。所以,在我们的情况下,要将电池状态插件添加到项目中,我们将运行以下命令:
       ionic plugin add cordova-plugin-battery-status.

这将向我们当前的项目中添加电池状态插件(github.com/apache/cordova-plugin-battery-status)。在 Ionic Native 的文档中也可以找到相同的内容:ionicframework.com/docs/native/battery-status/

要查看已安装的所有插件,运行以下命令:

ionic plugin ls

然后,你应该看到以下屏幕截图:

除了添加 Cordova 插件之外,我们还需要为电池状态添加所需的 Ionic Native 模块。运行以下命令:

npm install --save @ionic-native/battery-status

添加模块后,我们需要在example15/src/app/app.module.ts中将其标记为提供者。打开example15/src/app/app.module.ts并按照所示进行更新:

import { NgModule, ErrorHandler } from '@angular/core'; 
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular'; 
import { MyApp } from './app.component'; 
import { HomePage } from '../pages/home/home'; 

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

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

现在,我们可以开始使用电池状态插件。打开example15/src/pages/home/home.ts并使用以下代码进行更新:

import { Component } from '@angular/core'; 
import { BatteryStatus } from 'ionic-native'; 
import { Platform } from 'ionic-angular'; 

@Component({ 
  selector: 'page-home', 
  templateUrl: 'home.html' 
}) 
export class HomePage { 
  level: Number; 
  isPlugged: Boolean; 

  constructor(platform: Platform) { 
    platform.ready().then(() => { 
      BatteryStatus.onChange().subscribe( 
        (status) => { 
          this.level = status.level; 
          this.isPlugged = status.isPlugged; 
        } 
      ); 
    }); 
  } 
}

这就是 Ionic Native 如何公开BatteryStatus

接下来,按照以下方式更新example15/src/pages/home/home.html中的ion-content部分:

<ion-header> 
    <ion-navbar> 
        <ion-title> 
            Battery Status 
        </ion-title> 
    </ion-navbar> 
</ion-header> 
<ion-content padding> 
    <h2>level : {{level}}</h2> 
    <h2>isPluggedIn : {{isPlugged}}</h2> 
</ion-content>

现在运行以下命令:

ionic serve

你将在页面上看不到任何输出,如果你打开开发工具,你会在控制台中看到一个警告,上面写着:

Native: tried calling StatusBar.styleDefault, but Cordova is not 
available. 
Make sure to include cordova.js or run in a device/simulator

这意味着我们不能直接在浏览器中运行插件;它们需要一个环境来执行,比如 Android、iOS 或 Windows。

为了测试应用程序(和插件),我们将添加一个 Android 平台或一个 iOS 平台:

ionic platform add android

你也可以使用以下命令:

ionic platform add ios

然后执行以下命令之一:

  • ionic emulate android

  • ionic emulate ios

  • ionic run android

  • ionic run ios

运行任何一个前面的命令都会显示以下输出:

现在你知道如何向你的 Ionic 应用程序中添加 Cordova 插件并对其进行测试。在接下来的部分中,我们将使用更多的插件。来自 Genymotion 的前面的屏幕截图是我个人使用许可证的。这些图片仅用于说明目的。

Cordova 白名单插件

在继续使用 Ionic Native 之前,我们将花一些时间来了解一个关键的 Cordova 插件--白名单插件:github.com/apache/cordova-plugin-whitelist

从白名单插件的 Cordova 文档中:

“域白名单是一种安全模型,用于控制应用程序无法控制的外部域的访问。Cordova 提供了一个可配置的安全策略,用于定义可以访问哪些外部站点。”

因此,如果我们希望更好地控制我们的应用程序在处理来自其他来源的内容时的行为方式,我们应该使用白名单插件。您可能已经注意到,此插件已添加到我们的 Ionic 应用程序中。如果此插件尚未添加到 Ionic/Cordova 应用程序中,您可以通过运行以下命令轻松添加:

ionic plugin add https://github.com/apache/cordova-plugin-
whitelist.git

一旦添加了插件,您可以更新config.xml文件以进行导航白名单 - 允许您的应用程序在 WebView 内打开的链接,以允许链接到example.com

您将添加以下代码:

<allow-navigation href="http://example.com/*" />

如果要使您的 WebView 链接到任何网站,您需要添加以下内容:

<allow-navigation href="http://*/*" /> 
<allow-navigation href="https://*/*" /> 
<allow-navigation href="data:*" />

您还可以添加意图白名单,其中可以指定允许在设备上浏览的链接列表。例如,从我们的自定义应用程序中打开短信应用程序:

<allow-intent href="sms:*" />

或简单的网页:

<allow-intent href="https://*/*" />

您还可以使用此插件在应用程序上强制执行内容安全策略CSP)(content-securitypolicy.com/)。您只需要在www/index.html文件中添加meta标签,如下所示:

<!-- Allow XHRs via https only --> 
<meta http-equiv="Content-Security-Policy" content="default-src 'self' https:">

这是白名单插件的快速介绍。此插件适用于:

  • Android 4.0.0 或更高版本

  • iOS 4.0.0 或更高版本

请记住添加并配置此插件;否则,外部链接将无法工作。

使用 Ionic Native 处理 Cordova 插件

在之前的示例中,我们已经看到了如何将设备功能(例如电池状态)与我们的 Ionic 应用程序集成。现在,我们将探索更多类似的插件,并看看如何实现它们。

设备

我们将在本节中首先查看的插件是设备插件。此插件描述了设备的硬件和软件规格。

您可以在此处了解有关此插件的更多信息:github.com/apache/cordova-plugin-deviceionicframework.com/docs/native/device/

让我们搭建一个新的空白应用程序,然后向其中添加设备插件:

ionic start -a "Example 16" -i app.example.sixteen example16 blank --v2

应用程序搭建完成后,cd进入example16文件夹。现在我们将添加设备插件,运行以下命令:

ionic plugin add cordova-plugin-device

这将添加设备插件。完成后,我们将添加 Ionic 本机设备模块。运行以下命令:

npm install --save @ionic-native/device

添加模块后,我们需要在example16/src/app/app.module.ts中将其标记为提供者。按照以下方式更新example16/src/app/app.module.ts

import { NgModule, ErrorHandler } from '@angular/core'; 
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular'; 
import { MyApp } from './app.component'; 
import { HomePage } from '../pages/home/home'; 

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

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

接下来,通过运行ionic platform add iosionic platform add android来添加 iOS 或 Android 平台之一。

现在,我们将添加与设备插件相关的代码。打开example16/src/pages/home/home.ts并按照以下方式更新类:

import { Component } from '@angular/core'; 
import { Device } from '@ionic-native/device'; 
import { Platform } from 'ionic-angular'; 

@Component({ 
  selector: 'page-home', 
  templateUrl: 'home.html' 
}) 
export class HomePage { 
  cordova: String; 
  model: String; 
  devicePlatform: String; 
  uuid: String; 
  version: String; 
  manufacturer: String; 
  isVirtual: Boolean; 
  serial: String; 

  constructor(private platform: Platform, 
    private device: Device) { 
    platform.ready().then(() => { 
      let device = this.device; 
      this.cordova = device.cordova; 
      this.model = device.model; 
      this.devicePlatform = device.platform; 
      this.uuid = device.uuid; 
      this.version = device.version; 
      this.manufacturer = device.manufacturer; 
      this.isVirtual = device.isVirtual; 
      this.serial = device.serial; 
    }); 
  } 
}

接下来,按照以下方式更新example16/src/pages/home/home.html

<ion-header> 
  <ion-navbar> 
    <ion-title> 
      Ionic Blank 
    </ion-title> 
  </ion-navbar> 
</ion-header> 

<ion-content padding> 
  <table> 
    <tr> 
      <td>cordova</td> 
      <td>{{cordova}}</td> 
    </tr> 
    <tr> 
      <td>model</td> 
      <td>{{model}}</td> 
    </tr> 
    <tr> 
      <td>platform</td> 
      <td>{{platform}}</td> 
    </tr> 
    <tr> 
      <td>uuid</td> 
      <td>{{uuid}}</td> 
    </tr> 
    <tr> 
      <td>version</td> 
      <td>{{version}}</td> 
    </tr> 
    <tr> 
      <td>manufacturer</td> 
      <td>{{manufacturer}}</td> 
    </tr> 
    <tr> 
      <td>isVirtual</td> 
      <td>{{isVirtual}}</td> 
    </tr> 
    <tr> 
      <td>serial</td> 
      <td>{{serial}}</td> 
    </tr> 
  </table> 
</ion-content>

保存所有文件,最后运行ionic emulate iosionic emulate android。我们应该看到以下输出:

如前面的屏幕截图所示,设备是 Nexus 6P。

吐司

我们将要使用的下一个插件是 Toast 插件。此插件显示文本弹出窗口,不会阻止用户与应用程序的交互。

您可以在这里了解有关此插件的更多信息:github.com/EddyVerbruggen/Toast-PhoneGap-Pluginionicframework.com/docs/native/toast/

我们将使用以下命令搭建一个新的空白应用程序:

ionic start -a "Example 17" -i 
app.example.seventeen example17 blank --v2

应用程序搭建完成后,cd进入example17文件夹。现在我们将添加 Toast 插件,运行:

ionic plugin add cordova-plugin-x-toast

然后我们将添加 Ionic Native Toast 模块:

npm install --save @ionic-native/toast

接下来,我们将在example17/src/app/app.module.ts中将 Toast 添加为提供者。按照以下方式更新example17/src/app/app.module.ts

import { NgModule, ErrorHandler } from '@angular/core'; 
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular'; 
import { MyApp } from './app.component'; 
import { HomePage } from '../pages/home/home'; 

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

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

完成后,通过运行以下命令添加 iOS 或 Android 平台之一:

ionic platform add ios

或:

ionic platform add android

现在,我们将添加与 Toast 插件相关的代码。打开example17/src/pages/home/home.ts并按照文件中所示进行更新:

import { Component } from '@angular/core'; 
import { Toast } from '@ionic-native/toast'; 
import { Platform } from 'ionic-angular'; 

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

  constructor(private platform: Platform, private toast: Toast) { 
    platform.ready().then(() => { 
      toast.show("I'm a toast", '5000', 'center').subscribe( 
        (toast) => { 
          console.log(toast); 
        } 
      ); 
  }); 
  } 

}

保存所有文件并运行:

ionic emulate ios or ionic emulate android

您应该看到以下输出:

要了解有关 Toast 插件 API 方法的更多信息,请参阅:ionicframework.com/docs/native/toast/

对话框

我们接下来要使用的插件是对话框插件。这会触发警报、确认和提示窗口。

您可以从这里了解有关插件的更多信息:github.com/apache/cordova-plugin-dialogsionicframework.com/docs/native/dialogs/

首先,为对话框插件搭建一个新的空白应用程序:

ionic start -a "Example 18" -i app.example.eightteen example18 blank --v2

应用程序搭建完成后,cd进入example18文件夹。现在,我们将添加对话框插件,运行:

ionic plugin add cordova-plugin-dialogs

之后,我们将为对话框添加 Ionic Native 模块。运行以下命令:

npm install --save @ionic-native/dialogs

接下来,将对话框添加为提供者。按照以下步骤更新example18/src/app/app.module.ts

import { NgModule, ErrorHandler } from '@angular/core'; 
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular'; 
import { MyApp } from './app.component'; 
import { HomePage } from '../pages/home/home'; 

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

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

完成后,通过运行以下命令添加 iOS 或 Android 平台之一:

ionic platform add ios

或者:

ionic platform add android

现在,我们将添加与对话框插件相关的代码。打开example18/src/pages/home/home.ts并更新为所述的代码文件:

import { Component } from '@angular/core'; 
import { Dialogs } from '@ionic-native/dialogs'; 
import { Platform } from 'ionic-angular'; 

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

  constructor(private dialogs: Dialogs, private platform: Platform) { 
    platform.ready().then(() => { 
      dialogs 
        .prompt('Name Please?', 'Identity', ['Cancel', 'Ok'], 'John 
        McClane') 
        .then((result) => { 
          if (result.buttonIndex == 2) { 
            this.name = result.input1; 
          } 
        }); 
    }); 
  } 
}

接下来,我们将按照以下方式更新example18/src/pages/home/home.html

<ion-header> 
    <ion-navbar> 
        <ion-title> 
            Reveal Your Identity 
        </ion-title> 
    </ion-navbar> 
</ion-header> 
<ion-content padding> 
    Hello {{name}}!! 
</ion-content>

保存所有文件,最后运行以下命令:

    ionic emulate ios or ionic emulate android

我们应该看到以下输出:

要了解有关对话框插件 API 方法的更多信息,请参阅:ionicframework.com/docs/native/dialogs/

本地通知

我们接下来要使用的插件是本地通知插件。该插件主要用于通知或提醒用户与应用相关的活动。有时,当后台活动正在进行时,也会显示通知,比如大文件上传。

您可以从这里了解有关插件的更多信息:github.com/katzer/cordova-plugin-local-notificationsionicframework.com/docs/native/local-notifications/

首先,为本地通知插件搭建一个新的空白应用程序:

ionic start -a "Example 19" -i 
app.example.nineteen example19 blank --v2

应用程序搭建完成后,cd进入example19文件夹。现在,我们将添加本地通知插件,运行以下命令:

ionic plugin add de.appplant.cordova.plugin.local-notification

接下来,添加 Ionic Native 模块:

npm install --save @ionic-native/local-notifications

并在example19/src/app/app.module.ts中更新提供者:

import { NgModule, ErrorHandler } from '@angular/core'; 
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular'; 
import { MyApp } from './app.component'; 
import { HomePage } from '../pages/home/home'; 

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

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

完成后,通过运行以下命令添加 iOS 或 Android 平台之一:

ionic platform add ios

或者:

ionic platform add android

现在,我们将添加与本地通知插件相关的代码。打开example19/src/pages/home/home.ts并按照以下方式更新:

import { Component } from '@angular/core'; 
import { LocalNotifications } from '@ionic-native/local-notifications'; 
import { Platform } from 'ionic-angular'; 

@Component({ 
  selector: 'page-home', 
  templateUrl: 'home.html' 
}) 
export class HomePage { 
  defaultText: String = 'Hello World'; 

  constructor(private localNotifications: LocalNotifications, private platform: Platform) { } 

  triggerNotification(notifText) { 
    this.platform.ready().then(() => { 

      notifText = notifText || this.defaultText; 
      this.localNotifications.schedule({ 
        id: 1, 
        text: notifText, 
      }); 
    }); 
  } 

}

接下来,我们将按照以下方式更新example19/src/pages/home/home.html

<ion-header> 
    <ion-navbar> 
        <ion-title> 
            Local Notification 
        </ion-title> 
    </ion-navbar> 
</ion-header> 
<ion-content padding> 
    <div class="list"> 
        <label class="item item-input"> 
            <span class="input-label">Enter Notification text</span> 
            <input type="text" #notifText [ngModel]="defaultText"> 
        </label> 
        <label class="item item-input"> 
            <button ion-button color="dark" (click)=" 
            triggerNotification(notifText.value)">Notify</button> 
        </label> 
    </div> 
</ion-content>

保存所有文件,然后运行以下命令:

ionic server android

或者:

ionic server ios

我们应该看到以下输出:

现在,当我们查看通知栏时,应该看到本地通知:

要了解有关对话框插件 API 方法的更多信息,请参阅:ionicframework.com/docs/native/local-notifications/

地理位置

我们要查看的最后一个插件是地理位置插件,它可以帮助获取设备的坐标。

您可以从这里了解有关插件的更多信息:github.com/apache/cordova-plugin-geolocationionicframework.com/docs/native/geolocation/

首先,为地理位置插件搭建一个新的空白应用程序:

ionic start -a "Example 20" -i app.example.twenty example20 blank --v2

应用程序搭建完成后,cd进入example20文件夹。现在,我们将添加地理位置插件,运行以下命令:

ionic plugin add cordova-plugin-geolocation

接下来,运行以下命令以添加 Ionic Native 模块:

npm install --save @ionic-native/geolocation

现在,我们注册提供者。更新example20/src/app/app.module.ts

import { NgModule, ErrorHandler } from '@angular/core'; 
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular'; 
import { MyApp } from './app.component'; 
import { HomePage } from '../pages/home/home'; 

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

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

完成后,通过运行以下命令添加 iOS 或 Android 平台之一:

ionic platform add ios

或者:

ionic platform add android

现在,我们将添加与地理位置插件相关的代码。打开example20/src/pages/home/home.ts并更新如下:

import { Component } from '@angular/core'; 
import { Platform } from 'ionic-angular'; 
import { Geolocation } from '@ionic-native/geolocation'; 

@Component({ 
  selector: 'page-home', 
  templateUrl: 'home.html' 
}) 
export class HomePage { 
  latitude: Number = 0; 
  longitude: Number = 0; 
  accuracy: Number = 0; 

  constructor(private platform: Platform, 
    private geolocation: Geolocation) { 
    platform.ready().then(() => { 
      geolocation.getCurrentPosition().then((position) => { 
        this.latitude = position.coords.latitude; 
        this.longitude = position.coords.longitude; 
        this.accuracy = position.coords.accuracy; 
      }); 
    }); 
  } 
}

接下来,按照以下代码更新example20/src/pages/home/home.html

<ion-header> 
    <ion-navbar> 
        <ion-title> 
            Ionic Blank 
        </ion-title> 
    </ion-navbar> 
</ion-header> 
<ion-content padding> 
    <ul class="list"> 
        <li class="item"> 
            Latitude : {{latitude}} 
        </li> 
        <li class="item"> 
            Longitude : {{longitude}} 
        </li> 
        <li class="item"> 
            Accuracy : {{accuracy}} 
        </li> 
    </ul> 
</ion-content>

保存所有文件,最后运行以下命令:

ionic emulate ios

或:

ionic emulate android

我们应该能够看到以下输出:

一旦权限被提供,我们应该能够看到以下截图中显示的值:

我的 Google Nexus 6P 运行 Android Nougat,其中有一个名为运行时权限的新功能。这允许用户在运行时而不是在安装应用程序时给予权限。您可以在这里了解更多关于该功能的信息:developer.android.com/training/permissions/requesting.html

要了解有关 Geolocation 插件 API 方法的更多信息,请参考:ionicframework.com/docs/native/geolocation/

前面的例子应该已经很好地展示了你如何使用 Ionic Native。

摘要

在本章中,我们已经了解了 Cordova 插件是什么,以及它们如何在现有的 Ionic 应用程序中使用。我们首先建立了一个用于 Android 和 iOS 的本地开发环境,然后学习了如何模拟和运行 Ionic 应用程序。接下来,我们探索了如何将 Cordova 插件添加到 Ionic 项目中并使用它们。最后,借助 Ionic Native,我们在 Ionic 应用程序中注入了插件并与它们一起工作。

在下一章中,我们将利用到目前为止所学到的知识来构建一个名为 Riderr 的应用程序。利用 Uber 提供的公共 API,我们将构建一个应用程序,通过它,乘客可以预订 Uber 车辆。

第七章:构建 Riderr 应用程序

根据我们迄今所学的知识,我们将构建一个帮助用户预订行程的应用程序。该应用程序使用 Uber 提供的 API(uber.com/),这是一个流行的叫车服务提供商,并将其与 Ionic 应用程序集成。在这个应用程序中,我们将处理以下内容:

  • 集成 Uber OAuth 2.0

  • 集成 REST API

  • 与设备功能交互

  • 使用 Google API

  • 最后,预订行程

本章的主要目的是展示如何同时使用 REST API 和设备功能,如地理位置和 InappBrowser,来使用 Ionic 构建真实世界的应用程序。

应用程序概述

我们将要构建的应用程序名为 Riderr。Riderr 帮助用户在两个地点之间预订出租车。该应用程序使用 Uber 提供的 API(uber.com/)来预订行程。在这个应用程序中,我们不会集成 Uber 的所有 API。我们将实现一些端点,显示用户的信息以及用户的行程信息,以及一些帮助我们预订行程、查看当前行程和取消行程的端点。

为了实现这一点,我们将使用 Uber 的 OAuth 来对用户进行认证,以便我们可以显示用户的信息并代表用户预订行程。

这是一个快速预览,一旦我们完成应用程序的构建,它将会是什么样子:

注意:无论是图书出版公司还是我都不对由于使用 Uber 生产 API 而导致的金钱损失或账户禁止负责。请在使用 Uber 生产 API 之前仔细阅读 API 说明。

Uber API

在这一部分,我们将介绍我们将在 Riderr 应用程序中使用的各种 API。我们还将生成一个客户端 ID、客户端密钥和服务器令牌,我们将在发出请求时使用。

认证

访问 Uber API 有三种认证机制:

  • 服务器令牌

  • 单点登录(SSO)

  • OAuth 2.0

为了代表用户发出请求、访问用户的个人信息并代表用户预订行程,我们需要一个 OAuth 2.0 访问令牌。因此,我们将遵循 OAuth 2.0 机制。

如果您对 OAuth 2.0 机制不熟悉,请参阅www.bubblecode.net/en/2016/01/22/understanding-oauth2/www.digitalocean.com/community/tutorials/an-introduction-to-oauth-2

在 Uber 注册

在我们进一步进行之前,我们需要一个 Uber 账户来登录并在 Uber 注册一个新的应用程序。如果您没有账户,您可以使用 Uber 应用程序很容易地创建一个。

一旦您创建了 Uber 账户,导航至developer.uber.com/dashboard/create,登录并填写以下表格:

然后点击创建。这将在 Uber 注册一个新的应用程序,并为该应用程序创建一个客户端 ID、客户端密钥和服务器令牌。接下来,在同一页面上点击授权选项卡(我们在那里找到客户端 ID)。将重定向 URL 更新为http://localhost/callback。这非常重要。如果我们不这样做,Uber 就不知道在认证后将用户发送到哪里。

使用客户端 ID 和客户端密钥的组合,我们请求访问令牌。然后,使用这个访问令牌,我们将代表用户访问 Uber 资源。

为了进一步进行,您需要对 OAuth 2.0 有一个相当好的理解,因为我们将在我们的应用程序中实现它。

API

在这个应用程序中,我们将从 Uber 使用以下 API:

注意:您可以参考developer.uber.com/docs/riders/introduction获取其他可用的 API。

构建 Riderr

现在我们已经了解了 API 列表,我们将开始使用 Ionic 应用程序。

应用程序的脚手架

本章的下一步是搭建一个新的 Ionic 空白应用程序,并开始集成 Uber API。

创建一个名为chapter7的新文件夹,在chapter7文件夹内打开一个新的命令提示符/终端,并运行以下命令:

ionic start -a "Riderr" -i app.example.riderr riderr blank --v2

这将为我们搭建一个新的空白项目。

Uber API 服务

在本节中,我们将开始使用与 Uber API 接口的服务层进行工作。我们将在 Ionic 应用程序内实现上述端点。

应用程序搭建完成后,进入src文件夹并创建一个名为services的新文件夹。在services文件夹内,创建一个名为uber.service.ts的文件。我们将在这里编写所有 Uber 集成逻辑。

在您喜欢的文本编辑器中打开riderr项目,并导航到riderr/src/services/uber.service.ts。我们要做的第一件事是添加所需的导入。将以下内容添加到uber.services.ts文件的顶部:

import { Injectable } from '@angular/core'; 
import { LoadingController } from 'ionic-angular'; 
import { Http, Headers, Response, RequestOptions } from '@angular/http'; 
import { InAppBrowser } from '@ionic-native/in-app-browser'; 
import { Storage } from '@ionic/storage'; 
import { Observable } from 'rxjs/Observable';

我们已经包括

  • Injectable:将当前类标记为提供程序

  • LoadingController:在进行网络请求时显示消息;HttpHeadersResponseRequestOptions用于处理http请求

  • InAppBrowser:实现 OAuth 2.0 而不使用服务器获取访问令牌

  • 存储:用于存储访问令牌

  • Observable:用于更好地处理异步请求

接下来,我们将定义类和类级变量:

@Injectable() 
export class UberAPI { 
  private client_secret: string = 'igVTjJAByDAVfKYgaNGX1MgvoWNmsuTI_OYJz7eq'; 
  private client_id: string = '9i2dK88Ovw0WvH3wmS-H0JA6ZF5Z2GP1'; 
  private redirect_uri: string = 'http://localhost/callback'; 
  private scopes: string = 'profile history places request'; 
  // we will be using the sandbox URL for our app 
  private UBERSANDBOXAPIURL = 'https://sandbox-api.uber.com/v1.2/'; 
  // private UBERAPIURL = 'https://api.uber.com/v1.2/'; 
  private TOKENKEY = 'token'; // name of the key in storage 
  private loader; // reference to the loader 
  private token; // copy of token in memory 
}

client_secret and client_id from the new app you have registered with Uber. Do notice the scopes variable. It is here that we are requesting permission to access privileged content from Uber on the user's behalf.

注意:完成此示例后,我将删除前面注册的应用程序。因此,请确保您拥有自己的client_secretclient_id

接下来是构造函数:

//snipp -> Inside the class 
    constructor(private http: Http, 
    private storage: Storage, 
    private loadingCtrl: LoadingController, 
    private inAppBrowser: InAppBrowser) { 
      // fetch the token on load 
      this.storage.get(this.TOKENKEY).then((token) => { 
        this.token = token; 
      }); 
    }

constructor中,我们已经实例化了HttpStorageLoadingController类,我们还从内存中获取访问令牌并将其保存在内存中以供将来使用。

对于我们向 Uber API 发出的每个请求(除了认证请求),我们需要将访问令牌作为标头的一部分发送。我们有以下方法将帮助我们完成这一点:

// snipp 
  private createAuthorizationHeader(headers: Headers) { 
    headers.append('Authorization', 'Bearer ' + this.token); 
    headers.append('Accept-Language', 'en_US'); 
    headers.append('Content-Type', 'application/json'); 
  }

接下来,我们需要一个方法,返回一个布尔值,指示用户是否已经认证并且我们有一个令牌可以向 Uber API 发出请求:

// snipp 
  isAuthenticated(): Observable<boolean> { 
    this.showLoader('Autenticating...'); 
    return new Observable<boolean>((observer) => { 
      this.storage.ready().then(() => { 
        this.storage.get(this.TOKENKEY).then((token) => { 
          observer.next(!!token); // !! -> converts truthy falsy to 
          boolean. 
          observer.complete(); 
          this.hideLoader(); 
        }); 
      }); 
    }); 
  }

此方法将查询存储中是否存在令牌。如果令牌存在,observer返回true,否则返回false。我们将在所有 API 的末尾实现showLoader()hideLoader()

如果用户已经认证,用户已登录。这意味着我们需要一个选项,用户退出登录。由于 API 服务器是无状态的,它不维护任何会话信息以使其失效。因此,通过从存储中清除令牌,我们使客户端端的会话失效:

// snipp 
  logout(): Observable<boolean> { 
    return new Observable<boolean>((observer) => { 
      this.storage.ready().then(() => { 
        this.storage.set(this.TOKENKEY, undefined); 
        this.token = undefined; 
        observer.next(true); 
        observer.complete(); 
      }); 
    }); 
  }

现在我们将编写我们的第一个与 Uber API 交互的 API 方法。这是认证方法:

// snipp 
auth(): Observable<boolean> { 
    return new Observable<boolean>(observer => { 
      this.storage.ready().then(() => { 
        let browser = 
        this.inAppBrowser.create
        (`https://login.uber.com/oauth/v2/authorize?           
        client_id=${this.client_id}&
        response_type=code&scope=${this.scopes}
        &redirect_uri=${this.redirect_uri}`, '_blank',  
        'location=no,clearsessioncache=yes,clearcache=yes'); 
        browser.on('loadstart').subscribe((event) => { 
          let url = event.url; 

          // console.log(url); 
          // URLS that get fired 

          // 1\. https://login.uber.com/oauth/v2/authorize?
          client_id=9i2dK88Ovw0WvH3wmS-
          H0JA6ZF5Z2GP1&response_type=
          code&scope=profile%20history%20places%20request

          // 2\. https://auth.uber.com/login/? 
          next_url=https%3A%2F%2Flogin.uber.com
          %2Foauth%...520places%2520request
          &state=Pa2ONzlEGsB4M41VLKOosWTlj9snJqJREyCFrEhfjx0%3D 

          // 3\. https://login.uber.com/oauth/v2/authorize?
          client_id=9i2dK88Ovw0WvH3wmS-
          H0JA...ry%20places%20request&
          state=Pa2ONzlEGsB4M41VLKOosWTlj9snJqJREyCFrEhfjx0%3D 

          // 4\. http://localhost/callback?state=
          Pa2ONzlEGsB4M41VLKOosWTlj9snJqJREyCFrEhfjx0%3D&
          code=9Xu6ueaNhUN1uZVvqvKyaXPhMj8Bzb#_ 

          // we are interested in #4 
          if (url.indexOf(this.redirect_uri) === 0) { 
            browser.close(); 
            let resp = (url).split("?")[1]; 
            let responseParameters = resp.split("&"); 
            var parameterMap: any = {}; 

            for (var i = 0; i < responseParameters.length; i++) { 
              parameterMap[responseParameters[i].split("=")[0]] = 
              responseParameters[i].split("=")[1]; 
            } 

            // console.log('parameterMap', parameterMap); 
            /* 
              { 
                "state": 
                "W9Ytf2cicTMPMpMgwh9HfojKv7gQxxhrcOgwffqdrUM%3D", 
                "code": "HgSjzZHfF4GaG6x1vzS3D96kGtJFNB#_" 
              } 
            */ 

            let headers = new Headers({ 
              'Content-Type': "application/x-www-form-urlencoded" 
            }); 
            let options = new RequestOptions({ headers: headers }); 
            let data = 
            `client_secret=${this.client_secret}
            &client_id=${this.client_id}&grant_type=
            authorization_code&redirect_uri=
            ${this.redirect_uri}&code=${parameterMap.code}`; 

            return 
            this.http.post
            ('https://login.uber.com/oauth/v2/token', data, options) 
              .subscribe((data) => { 
                let respJson: any = data.json(); 
                // console.log('respJson', respJson); 
                /* 
                  { 
                    "last_authenticated": 0, 
                    "access_token": "snipp", 
                    "expires_in": 2592000, 
                    "token_type": "Bearer", 
                    "scope": "profile history places request", 
                    "refresh_token": "26pgA43ZvQkxEQi7qYjMASjfq6lg8F" 
                  } 
                */ 

                this.storage.set(this.TOKENKEY, respJson.access_token); 
                this.token = respJson.access_token; // load it up in 
                memory 
                observer.next(true); 
                observer.complete(); 
              }); 
          } 
        }); 
      }); 
    }); 
  }

在这种方法中发生了很多事情。我们使用 Ionic Native 的 InAppBrowser(ionicframework.com/docs/native/in-app-browser/)插件将用户重定向到授权端点。授权端点(https://login.uber.com/oauth/v2/authorize?client_id=${this.client_id}&response_type=code&scope=${this.scopes}&redirect_uri=${this.redirect_uri})需要客户端 ID,范围和重定向 URL。

redirect_uri是一个重要的参数,因为 Uber API 在认证后将应用程序重定向到该 URL。在我们的应用程序内部,我们通过browser.on('loadstart')监听 URL 更改事件。我们正在寻找以http://localhost/callback开头的 URL。如果匹配此 URL,我们将关闭浏览器并从 URL 中提取代码。

一旦我们获得代码,我们需要交换相同的代码以获得访问令牌。这将是auth()的下一部分,通过传递client_secretclient_idredirect_uricodehttps://login.uber.com/oauth/v2/token获取令牌。一旦我们收到访问令牌,我们将其保存到存储中。

注意:要了解更多关于存储的信息,请参考ionicframework.com/docs/storage/第四章中的存储服务部分。

现在我们有了访问令牌,我们将向 Uber API 发出请求以获取、发布和删除数据。

我们要实现的第一个 API 方法将用于获取用户的信息:

// snipp 
  getMe(): Observable<Response> { 
    this.showLoader(); 
    let headers = new Headers(); 
    this.createAuthorizationHeader(headers); 
    return this.http.get(this.UBERSANDBOXAPIURL + 'me', { 
      headers: headers 
    }); 
  }

请注意,我正在向 Uber Sandbox API URL 发出 API 请求,而不是向生产服务发出请求。在您对实施有信心之前,这总是一个好主意。Uber Sandbox API 和 Uber API 具有非常相似的实施,除了沙箱环境中的数据不是实时的,它遵循与 Uber API 相同的规则。在生产环境中,请记住更新 API 基础。

接下来是历史 API:

// snipp 
  getHistory(): Observable<Response> { 
    this.showLoader(); 
    let headers = new Headers(); 
    this.createAuthorizationHeader(headers); 
    return this.http.get(this.UBERSANDBOXAPIURL + 'history', { 
      headers: headers 
    }); 
  }

标头将传递给每个需要访问令牌来处理请求的请求。

接下来是支付方式端点:

// snipp 
  getPaymentMethods(): Observable<Response> { 
    this.showLoader(); 
    let headers = new Headers(); 
    this.createAuthorizationHeader(headers); 
    return this.http.get(this.UBERSANDBOXAPIURL + 'payment-methods', { 
      headers: headers 
    }); 
  }

前面三个端点将返回用户和用户乘车信息。下一个端点将返回在给定位置支持的产品列表:

// snipp 
  getProducts(lat: Number, lon: Number): Observable<Response> { 
    this.showLoader(); 
    let headers = new Headers(); 
    this.createAuthorizationHeader(headers); 
    return this.http.get(this.UBERSANDBOXAPIURL + 'products?latitude=' 
    + lat + '&longitude=' + lon, { 
      headers: headers 
    }); 
  }

此方法将用于显示可用的产品或乘车类型的列表。

在实际预订行程之前,我们需要先获取费用估算。我们将使用requestRideEstimates()方法来实现这一点:

//snipp 
  requestRideEstimates(start_lat: Number, end_lat: Number, start_lon: Number, end_lon: Number): Observable<Response> { 
    this.showLoader(); 
    // before booking 
    let headers = new Headers(); 
    this.createAuthorizationHeader(headers); 
    return this.http.post(this.UBERSANDBOXAPIURL + 'requests/estimate', { 
      "start_latitude": start_lat, 
      "start_longitude": start_lon, 
      "end_latitude": end_lat, 
      "end_longitude": end_lon 
    }, { headers: headers }); 
  }

一旦我们获得了费用估算并且用户接受了它,我们将使用requestRide()发起预订请求:

// snipp 
  requestRide(product_id: String, fare_id: String, start_lat: Number, end_lat: Number, start_lon: Number, end_lon: Number): Observable<Response> { 
    this.showLoader(); 
    let headers = new Headers(); 
    this.createAuthorizationHeader(headers); 
    return this.http.post(this.UBERSANDBOXAPIURL + 'requests', { 
      "product_id": product_id, 
      "fare_id": fare_id, 
      "start_latitude": start_lat, 
      "start_longitude": start_lon, 
      "end_latitude": end_lat, 
      "end_longitude": end_lon 
    }, { headers: headers }); 
  }

该方法返回预订的状态。在沙箱环境中,不会预订乘车。如果您真的想要预订实际的乘车,您可以更改 API URL 并发起实际的预订。请记住,Uber 司机将真正给您打电话来接您。如果您取消乘车,将收取适当的取消费用。

注意:图书出版公司和我都不对由 Uber 导致的金钱损失或帐户禁止负责。在使用 Uber 生产 API 之前,请仔细阅读 API 说明。

由于 Uber 只允许从一个帐户一次预订一次乘车,我们可以使用getCurrentRides()来获取当前乘车信息:

//snipp 
  getCurrentRides(lat: Number, lon: Number): Observable<Response> { 
    this.showLoader(); 
    let headers = new Headers(); 
    this.createAuthorizationHeader(headers); 
    return this.http.get(this.UBERSANDBOXAPIURL + 'requests/current', { 
      headers: headers 
    }); 
  }

最后,要取消乘车,我们将使用cancelCurrentRide()发出删除请求:

// snipp 
  cancelCurrentRide(): Observable<Response> { 
    this.showLoader(); 
    let headers = new Headers(); 
    this.createAuthorizationHeader(headers); 
    return this.http.delete(this.UBERSANDBOXAPIURL + 
    'requests/current', { 
      headers: headers 
    }); 
  }

显示和隐藏处理加载程序的两个实用方法如下:

// snipp 
private showLoader(text?: string) { 
    this.loader = this.loadingCtrl.create({ 
      content: text || 'Loading...' 
    }); 
    this.loader.present(); 
  } 

  public hideLoader() { 
    this.loader.dismiss(); 
  }

有了这个,我们已经添加了所有我们将用来与 Uber API 交互的必需 API。

集成

现在我们已经有了所需的 API 服务,我们将创建所需的视图来表示这些数据。

当我们搭建应用程序时,将为我们创建一个名为home的页面。但是,由于在我们的应用程序中,一切都从认证开始,我们将首先生成一个登录页面。然后我们将使其成为应用程序的第一个页面。要生成一个新页面,请运行以下命令:

ionic generate page login

接下来,我们需要更新riderr/src/app/app.module.ts中的页面引用。按照所示更新@NgModule

import { NgModule, ErrorHandler } from '@angular/core'; 
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular'; 
import { MyApp } from './app.component'; 
import { HomePage } from '../pages/home/home'; 
import { LoginPage } from '../pages/login/login'; 

import { UberAPI } from '../services/uber.service'; 
import { IonicStorageModule } from '@ionic/storage'; 

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

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

随着我们的进展,我们将生成并添加剩余的页面。

注意:随着 Ionic 不断发展,页面的类名和结构可能会发生变化。但在 Ionic 中开发应用程序的要点将保持不变。

接下来,我们将更新app.component.ts以加载登录页面作为第一个页面。按照所示更新riderr/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 { LoginPage } from '../pages/login/login'; 

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

  constructor(platform: Platform, statusBar: StatusBar, splashScreen: SplashScreen) { 
    platform.ready().then(() => { 
      statusBar.styleDefault(); 
      splashScreen.hide(); 
    }); 
  }

现在我们将更新LoginPage组件。首先是login.html页面。按照所示更新riderr2/src/pages/login/login.html

<ion-content padding text-center> 
  <img src="img/logo.png" alt="Riderr Logo"> 
  <h2>Welcome to The Riderr App</h2> 
  <h3>This app uses Uber APIs to help you book a cab</h3> 
  <br><br><br> 
    <button ion-button color="primary" full (click)="auth()">Login with Uber</button> 
</ion-content>

您可以在这里找到logo.pngwww.dropbox.com/s/8tdfgizjm24l3nx/logo.png?dl=0。下载后,将图像移动到assets/icon文件夹中。

接下来,按照所示更新riderr/src/pages/login/login.ts

import { Component } from '@angular/core'; 
import { NavController } from 'ionic-angular'; 
import { UberAPI } from '../../services/uber.service'; 
import { HomePage } from '../home/home'; 

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

  constructor(private api: UberAPI, private navCtrl: NavController) { 
    // check if the user is already authenticated 
    this.api.isAuthenticated().subscribe((isAuth) => { 
      if (isAuth) { 
        this.navCtrl.setRoot(HomePage); 
      } 
      // else relax! 
    }); 
  } 

  auth() { 
    this.api.auth().subscribe((isAuthSuccess) => { 
      this.navCtrl.setRoot(HomePage); 
    }, function(e) { 
      // handle this in a user friendly way. 
      console.log('Fail!!', e); 
    }); 
  } 
}

在上述代码中,我们包括了所需的依赖项。在构造函数中,我们使用UberAPI类中创建的isAuthenticated()来检查用户是否已经验证。如果用户点击了 Uber 登录按钮,我们调用auth(),这将调用UberAPI类的auth()

如果用户成功验证,我们将用户重定向到“主页”。否则我们什么也不做。

假设用户已成功验证,用户将被重定向到主页。我们将基于主页的侧边菜单进行操作。侧边菜单将包含导航到应用程序中各种页面的链接。

我们将更新riderr/src/pages/home/home.html如下所示:

<ion-menu [content]="content" (ionClose)="ionClosed()" (ionOpen)="ionOpened()"> 
    <ion-header> 
        <ion-toolbar> 
            <ion-title>Menu</ion-title> 
        </ion-toolbar> 
    </ion-header> 
    <ion-content> 
        <ion-list> 
            <button ion-item menuClose 
            (click)="openPage(bookRidePage)"> 
                Book Ride 
            </button> 
            <button ion-item menuClose (click)="openPage(profilePage)"> 
                Profile 
            </button> 
            <button ion-item menuClose (click)="openPage(historyPage)"> 
                Rides 
            </button> 
            <button ion-item menuClose 
            (click)="openPage(paymentMethodsPage)"> 
                Payment Methods 
            </button> 
            <button ion-item menuClose (click)="logout()"> 
                Logout 
            </button> 
        </ion-list> 
    </ion-content> 
</ion-menu> 
<ion-nav #content [root]="rootPage" swipeBackEnabled="false"></ion-nav>

上述代码是不言自明的。要了解有关菜单的更多信息,请参阅ionicframework.com/docs/api/components/menu/Menu/

接下来,我们将更新HomePage类。如下所示更新riderr2/src/pages/home/home.ts

import { Component } from '@angular/core'; 
import { BookRidePage } from '../book-ride/book-ride'; 
import { ProfilePage } from '../profile/profile'; 
import { HistoryPage } from '../history/history'; 
import { PaymentMethodsPage } from '../payment-methods/payment-methods'; 
import { LoginPage } from '../login/login'; 
import { UberAPI } from '../../services/uber.service'; 
import { NavController, Events } from 'ionic-angular'; 
import { ViewChild } from '@angular/core'; 

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

  private rootPage; 
  private bookRidePage; 
  private profilePage; 
  private historyPage; 
  private paymentMethodsPage; 

  @ViewChild(BookRidePage) bookRide : BookRidePage; 

  constructor(private uberApi: UberAPI, 
    private navCtrl: NavController, 
    public events: Events) { 
    this.rootPage = BookRidePage; 

    this.bookRidePage = BookRidePage; 
    this.profilePage = ProfilePage; 
    this.historyPage = HistoryPage; 
    this.paymentMethodsPage = PaymentMethodsPage; 
  } 

  // http://stackoverflow.com/a/38760731/1015046 
  ionOpened() { 
    this.events.publish('menu:opened', ''); 
  } 

  ionClosed() { 
    this.events.publish('menu:closed', ''); 
  } 

  ngAfterViewInit() { 
    this.uberApi.isAuthenticated().subscribe((isAuth) => { 
      if (!isAuth) { 
        this.navCtrl.setRoot(LoginPage); 
        return; 
      } 
    }); 
  } 

  openPage(p) { 
    this.rootPage = p; 
  } 

  logout(){ 
    this.uberApi.logout().subscribe(() => { 
      this.navCtrl.setRoot(LoginPage); 
    }); 
  } 
}

在这里,我们已经导入了所需的类。我们将在接下来的几个步骤中生成缺失的页面。请注意@ViewChild()装饰器。当我们使用谷歌地图时,我们将通过它和ionOpened()ionClosed()进行操作。

视图初始化后,我们检查用户是否已经验证。如果没有,我们将用户重定向到登录页面。openPage()将根页面设置为菜单中选择的页面。logout()清除令牌并将用户重定向到登录页面。

现在我们将创建所需的页面。

首先,大部分操作发生的页面 - bookRide页面。运行以下命令:

ionic generate page bookRide

这将生成一个新页面。页面创建后,打开riderr/src/app/app.module.ts并将BookRidePage添加到@NgModule()declarationsentryComponents属性中。

BookRidePage是整个应用程序中最复杂的页面之一。首先,我们显示一个带有用户当前位置的谷歌地图。我们获取用户当前位置的可用产品并显示它们。

在我们进一步进行之前,我需要提到一个奇怪的 bug,当在 Ionic 应用程序中使用谷歌地图和地图上的点击事件时会发生。

在谷歌地图上,我们显示一个标记和一个带有用户当前位置的信息窗口。单击标记或信息窗口将重定向用户以设置目的地位置以预订乘车。为此,我们需要监听地图上的点击事件。当在非谷歌地图组件上工作时,如侧边菜单、警报等,这会导致问题。您可以在此处阅读有关该问题的更多信息:github.com/driftyco/ionic/issues/9942#issuecomment-280941997

因此,为了解决这个 bug,除了谷歌地图组件之外的任何点击交互,我们需要禁用谷歌地图上的点击监听器,一旦完成,我们需要重新启用它。

回到riderr/src/pages/home/home.ts中的ionOpened()ionClosed(),每当菜单打开或关闭时,我们都会从中触发自定义事件。这样,当菜单打开时,我们会禁用地图上的点击监听器,并在用户选择菜单项后启用点击监听器。在ionOpened()ionClosed()中,我们只触发了事件。我们将在riderr/src/pages/book-ride/book-ride.ts中处理相同的问题。

现在我们已经意识到了问题,我们可以进一步进行。我们将首先实现菜单和地图 HTML。更新riderr/src/pages/book-ride/book-ride.html如下所示:

<ion-header> 
    <ion-navbar> 
        <button ion-button menuToggle> 
            <ion-icon name="menu"></ion-icon> 
        </button> 
        <ion-title>Riderr</ion-title> 
        <ion-buttons end> 
            <button *ngIf="isRideinProgress" ion-button color="danger" 
            (click)="cancelRide()"> 
                Cancel Ride 
            </button> 
        </ion-buttons> 
    </ion-navbar> 
</ion-header> 
<ion-content> 
    <div #map id="map"></div> 
    <div class="prods-wrapper"> 
        <div *ngIf="!isRideinProgress"> 
            <h3 *ngIf="!products">Fetching Products</h3> 
            <ion-grid *ngIf="products"> 
                <ion-row> 
                    <ion-col *ngFor="let p of products" [ngClass]="
                    {'selected' : p.isSelected}"> 
                        <div class="br" (click)="productClick(p)"> 
                            <h3>{{p.display_name.replace('uber', '')}}
                            </h3> 
                        </div> 
                    </ion-col> 
                </ion-row> 
            </ion-grid> 
        </div> 
        <div *ngIf="isRideinProgress"> 
            <h3 text-center>Ride In Progress</h3> 
            <p text-center>Ideally the ride information would be 
            displayed here.</p> 
        </div> 
    </div> 
</ion-content>

在页眉中,我们有一个取消进行中乘车的按钮。我们将填充BookRidePage类中的isRideinProgress属性,该属性管理此处显示的页面状态。ion-grid组件显示了当前用户位置支持的产品列表。

还要注意,我们已经添加了<div #map id="map"></div>。这将是地图出现的地方。

为了清理 UI,我们将添加一些样式。按照以下方式更新riderr/src/pages/book-ride/book-ride.scss

page-book-ride { 
    #map { 
        height: 88%; 
    } 
    .prods-wrapper { 
        height: 12%; 
    } 
    .br { 
        padding: 3px; 
        text-align: center; 
    } 
    ion-col.selected { 
        color: #eee; 
        background: #333; 
    } 
    ion-col { 
        background: #eee; 
        color: #333; 
        border: 1px solid #ccc; 
    } 
    ion-col:last-child .br { 
        border: none; 
    } 
}

接下来,我们将更新BookRidePage类。有很多方法,所以我将按照执行顺序分几部分分享它们。

riderr/src/pages/book-ride/book-ride.ts中,我们将首先更新所需的导入:

import { Component } from '@angular/core'; 
import { UberAPI } from '../../services/uber.service'; 
import { 
  Platform, 
  NavController, 
  AlertController, 
  ModalController, 
  Events 
} from 'ionic-angular'; 
import { Diagnostic } from '@ionic-native/diagnostic'; 
import { Geolocation } from '@ionic-native/geolocation'; 
import { 
  GoogleMaps, 
  GoogleMap, 
  GoogleMapsEvent, 
  LatLng, 
  CameraPosition, 
  MarkerOptions, 
  Marker 
} from '@ionic-native/google-maps';  
import { AutocompletePage } from '../auto-complete/auto-complete';

@Component装饰器将保持不变。

接下来,我们将声明一些类级别的变量:

// snipp 
  private map: GoogleMap; 
  private products; 
  private fromGeo; 
  private toGeo; 
  private selectedProduct; 
  private isRideinProgress: boolean = false; 
  private currentRideInfo; 

然后定义构造函数:

  // snipp 
constructor(private uberApi: UberAPI, 
    private platform: Platform, 
    private navCtrl: NavController, 
    private alertCtrl: AlertController, 
    private modalCtrl: ModalController, 
    private diagnostic: Diagnostic, 
    private geoLocation: Geolocation, 
    private googleMaps: GoogleMap, 
    public events: Events) { }

一旦视图被初始化,使用ngAfterViewInit()钩子,我们将开始获取用户的地理位置:

// snipp 
ngAfterViewInit() { 
    //https://github.com/mapsplugin/cordova-plugin-googlemaps/issues/1140 
    this.platform.ready().then(() => { 
      this.requestPerms(); 

      //https://github.com/driftyco/ionic/issues/9942#issuecomment-
      280941997 
      this.events.subscribe('menu:opened', () => { 
        this.map.setClickable(false); 
      }); 
      this.events.subscribe('menu:closed', () => { 
        this.map.setClickable(true); 
      }); 
    }); 
  }

但在获取地理位置之前,我们需要请求用户允许我们访问位置服务。

还要注意为menu:openedmenu:closed事件实现的监听器。这是我们如何根据侧边菜单的状态禁用地图上的点击并重新启用它。继续我们的开发:

// snipp 
private requestPerms() { 
    let that = this; 
    function success(statuses) { 
      for (var permission in statuses) { 
        switch (statuses[permission]) { 
          case that.diagnostic.permissionStatus.GRANTED: 
            // console.log("Permission granted to use " + permission); 
            that.fetCords(); 
            break; 
          case that.diagnostic.permissionStatus.NOT_REQUESTED: 
            console.log("Permission to use " + permission + " has not 
            been requested yet"); 
            break; 
          case that.diagnostic.permissionStatus.DENIED: 
            console.log("Permission denied to use " + permission + " - 
            ask again?"); 
            break; 
          case that.diagnostic.permissionStatus.DENIED_ALWAYS: 
            console.log("Permission permanently denied to use " + 
            permission + " - guess we won't be using it then!"); 
            break; 
        } 
      } 
    } 

    function error(e) { 
      console.log(e); 
    } 

    this.diagnostic.requestRuntimePermissions([ 
      that.diagnostic.permission.ACCESS_FINE_LOCATION, 
      that.diagnostic.permission.ACCESS_COARSE_LOCATION 
    ]).then(success).catch(error); 
  }

使用来自@ionic-native/diagnostic的 Diagnostic 插件,我们请求运行时权限。这将显示一个弹出窗口,询问用户是否应用程序可以访问用户的地理位置。如果用户允许应用程序,我们将在成功回调中收到Diagnostic.permissionStatus.GRANTED状态。然后,我们将尝试获取用户的坐标。如果需要,其他情况可以得到优雅的处理:

// snipp 
  private isExecuted = false; 
  private fetCords() { 
    // this needs to be called only once 
    // since we are requesting 2 permission 
    // this will be called twice. 
    // hence the isExecuted 
    if (this.isExecuted) return; 
    this.isExecuted = true; 
    // maps api key : AzaSyCZhTJB1kFAP70RuwDts6uso9e3DCLdRWs 
    // ionic plugin add cordova-plugin-googlemaps --variable 
    API_KEY_FOR_ANDROID="AzaSyCZhTJB1kFAP70RuwDts6uso9e3DCLdRWs" 
    this.geoLocation.getCurrentPosition().then((resp) => { 
      // resp.coords.latitude 
      // resp.coords.longitude 
      // console.log(resp); 
      this.fromGeo = resp.coords; 
      // Get the products at this location 
      this.uberApi.getProducts(this.fromGeo.latitude, 
      this.fromGeo.longitude).subscribe((data) => { 
        this.uberApi.hideLoader(); 
        this.products = data.json().products; 
      }); 
      // Trip in progress? 
      this 
        .uberApi 
        .getCurrentRides(this.fromGeo.latitude, this.fromGeo.longitude) 
        .subscribe((crrRides) => { 
          this.currentRideInfo = crrRides.json(); 
          this.isRideinProgress = true; 
          this.uberApi.hideLoader(); 
          // check for existing rides before processing 
          this.loadMap(this.fromGeo.latitude, this.fromGeo.longitude); 
        }, (err) => { 
          if (err.status === 404) { 
            // no rides availble 
          } 
          this.isRideinProgress = false; 
          this.uberApi.hideLoader(); 
          // check for existing rides before processing 
          this.loadMap(this.fromGeo.latitude, this.fromGeo.longitude); 
        }); 
    }).catch((error) => { 
      console.log('Error getting location', error); 
    }); 
  }

fetCords()将使用 Geolocation Ionic Native 插件来获取用户的坐标。一旦我们收到位置,我们将发起一个请求来获取产品,传入用户的纬度和经度。同时,我们使用 Uber API 的getCurrentRides()来检查是否有正在进行的乘车。

一旦响应到达,我们将调用loadMap()来绘制所需的地图。

完成代码演示后,我们将安装所有必需的 Cordova 插件和 Ionic Native 模块:

// snipp 
private loadMap(lat: number, lon: number) { 
    let element: HTMLElement = document.getElementById('map'); 
    element.innerHTML = ''; 
    this.map = undefined; 
    this.map = this.googleMaps.create(element); 
    let crrLoc: LatLng = new LatLng(lat, lon); 
    let position: CameraPosition = { 
      target: crrLoc, 
      zoom: 18, 
      tilt: 30 
    }; 

    this.map.one(GoogleMapsEvent.MAP_READY).then(() => { 
      // move the map's camera to position 
      this.map.moveCamera(position); // works on iOS and Android 

      let markerOptions: MarkerOptions = { 
        position: crrLoc, 
        draggable: true, 
        title: this.isRideinProgress ? 'Ride in Progess' : 'Select 
        Destination >', 
        infoClick: (() => { 
          if (!this.isRideinProgress) { 
            this.selectDestination(); 
          } 
        }), 
        markerClick: (() => { 
          if (!this.isRideinProgress) { 
            this.selectDestination(); 
          } 
        }) 
      }; 

      this.map.addMarker(markerOptions) 
        .then((marker: Marker) => { 
          marker.showInfoWindow(); 
        }); 

      // a rare bug 
      // loader doesn't hide 
      this.uberApi.hideLoader(); 
    });
}

loadMap()获取用户的地理位置,创建一个标记在该位置,并使用相机 API 将视角移动到该点。标记上有一个简单的信息文本,选择目的地 >,当点击时,用户将进入一个屏幕以输入目的地来预订乘车。

infoClick()markerClick()注册一个回调来执行selectDestination(),只有当没有正在进行的乘车时:

// snipp 
  private productClick(product) { 
    // console.log(product); 
    // set the active product in the UI 
    for (let i = 0; i < this.products.length; i++) { 
      if (this.products[i].product_id === product.product_id) { 
        this.products[i].isSelected = true; 
      } else { 
        this.products[i].isSelected = false; 
      } 
    } 

    this.selectedProduct = product; 
  }

要预订乘车,用户应该选择一个产品。productClick()通过根据用户在主页上的选择设置产品为所选产品来处理这个问题。

一旦产品被选择并且用户的位置可用,我们可以要求用户输入目的地位置,以便我们可以检查车费估算:

// snipp 
private selectDestination() { 
    if (this.isRideinProgress) { 
      this.map.setClickable(false); 
      let alert = this.alertCtrl.create({ 
        title: 'Only one ride!', 
        subTitle: 'You can book only one ride at a time.', 
        buttons: ['Ok'] 
      }); 
      alert.onDidDismiss(() => { 
        this.map.setClickable(true); 
      }); 
      alert.present(); 
    } else { 
      if (!this.selectedProduct) { 
        // since the alert has a button 
        // we need to first stop the map from  
        // listening. Then process the alert 
        // then renable 
        this.map.setClickable(false); 
        let alert = this.alertCtrl.create({ 
          title: 'Select Ride', 
          subTitle: 'Select a Ride type to continue (Pool or Go or X)', 
          buttons: ['Ok'] 
        }); 
        alert.onDidDismiss(() => { 
          this.map.setClickable(true); 
        }); 
        alert.present(); 
      } else { 
        this.map.setClickable(false); 
        let modal = this.modalCtrl.create(AutoCompletePage); 
        modal.onDidDismiss((data) => { 
          this.map.setClickable(true); 
          this.toGeo = data; 
          this 
            .uberApi 
            .requestRideEstimates(this.fromGeo.latitude, 
             this.toGeo.latitude, this.fromGeo.longitude, 
             this.toGeo.longitude) 
            .subscribe((data) => { 
              this.uberApi.hideLoader(); 
              this.processRideFares(data.json()); 
            }); 

        }); 
        modal.present(); 
      } 
    } 
  }

selectDestination()负责目的地选择以及获取乘车估算。selectDestination()内部的第一个 if 条件是为了确保用户只有一个正在进行的乘车。第二个 if 条件检查是否至少有一个selectedProduct。如果一切顺利,我们将调用AutoCompletePage作为一个模态,用户可以使用 Google Places 服务搜索地点。一旦使用此服务选择了一个地点,我们将获取目的地的地理位置。然后将所需的信息传递给requestRideEstimates()来获取估算。

一旦我们完成了BookRidePage,我们将开始处理AutoCompletePage。当我们从requestRideEstimates()获取车费时,我们将向用户呈现相同的信息:

// snipp 
private processRideFares(fareInfo: any) { 
    // ask the user if the fare is okay,  
    // if yes, book the cab 
    // else, do nothing 
    console.log('fareInfo', fareInfo); 
    this.map.setClickable(false); 
    let confirm = this.alertCtrl.create({ 
      title: 'Book Ride?', 
      message: 'The fare for this ride would be ' 
      + fareInfo.fare.value 
      + ' ' + fareInfo.fare.currency_code + '.\n And it will take         
      approximately ' + 
      (fareInfo.trip.duration_estimate / 60) + ' mins.', 
      buttons: [ 
        { 
          text: 'No', 
          handler: () => { 
            this.map.setClickable(true); 
          } 
        }, 
        { 
          text: 'Yes', 
          handler: () => { 
            this.map.setClickable(true); 
            this 
              .uberApi 
              .requestRide(this.selectedProduct.product_id, 
               fareInfo.fare.fare_id, this.fromGeo.latitude, 
                this.toGeo.latitude, this.fromGeo.longitude, 
                this.toGeo.longitude) 
              .subscribe((rideInfo) => { 
                this.uberApi.hideLoader(); 
                // console.log('rideInfo', rideInfo.json()); 
                // Since we are making requests to the sandbox url 
                // the request will always be in processing. 
                // Once the request has been submitted, we need to  
                // keep polling the getCurrentRides() API 
                // to get the ride information 
                // WE ARE NOT GOING TO DO THAT! 
                this.isRideinProgress = true; 
                this.currentRideInfo = rideInfo.json(); 
              }); 
          } 
        } 
      ] 
    }); 
    confirm.present(); 
  }

processRideFares()以车费信息作为输入并向用户呈现车费。如果用户对车费和时间估计满意,我们会使用requestRide()向 Uber 发出预订乘车的请求。

最后,如果用户想要取消当前的乘车,我们提供cancelRide()

// snipp 
  private cancelRide() { 
    this 
      .uberApi 
      .cancelCurrentRide() 
      .subscribe((cancelInfo) => { 
        this.uberApi.hideLoader(); 
        this.isRideinProgress = false; 
        this.currentRideInfo = undefined; 
      }); 
  }

这将是一个调用cancelCurrentRide()

现在我们已经完成了BookRidePage所需的逻辑,我们将创建AutoCompletePage。运行以下命令:

ionic generate page autoComplete

完成后,我们需要将AutoCompletePage添加到riderr/src/app/app.module.ts中:

import { AutoCompletePage } from '../pages/auto-complete/auto-complete';

AutoCompletePage引用添加到@NgModule()declarationsentryComponents属性中。

AutoCompletePage类将包含与 Google Places 服务一起使用以搜索地点所需的逻辑。首先,我们将处理auto-complete.html。打开riderr/src/pages/auto-complete/auto-complete.html并按照以下方式更新它:

<ion-header> 
    <ion-toolbar> 
        <ion-title>Enter address</ion-title> 
        <ion-searchbar id="q" [(ngModel)]="autocomplete.query" [showCancelButton]="true" (ionInput)="updateSearch()" (ionCancel)="dismiss()"></ion-searchbar> 
    </ion-toolbar> 
</ion-header> 
<ion-content> 
    <ion-list> 
        <!-- (click) is buggy at times, hmmm? --> 
        <ion-item *ngFor="let item of autocompleteItems" tappable (click)="chooseItem(item)"> 
            {{ item.description }} 
        </ion-item> 
    </ion-list> 
</ion-content>

我们有一个搜索栏和一个ion-list来显示搜索结果。接下来,我们将处理auto-complete.ts。打开riderr/src/pages/auto-complete/auto-complete.ts并按照以下方式更新它:

import { Component, NgZone } from '@angular/core'; 
import { ViewController } from 'ionic-angular'; 

@Component({ 
  templateUrl: 'auto-complete.html' 
}) 

// http://stackoverflow.com/a/40854384/1015046 
export class AutocompletePage { 
  autocompleteItems; 
  autocomplete; 
  ctr: HTMLElement = document.getElementById("q"); 
  service = new google.maps.places.AutocompleteService(); 
  geocoder = new google.maps.Geocoder(); 

  constructor(public viewCtrl: ViewController, private zone: NgZone) { 
    this.autocompleteItems = []; 
    this.autocomplete = { 
      query: '' 
    }; 
  } 

  dismiss() { 
    this.viewCtrl.dismiss(); 
  } 

  chooseItem(item: any) { 
    // we need the lat long 
    // so we will make use of the  
    // geocoder service 
    this.geocoder.geocode({ 
      'placeId': item.place_id 
    }, (responses) => { 
      // send the place name 
      // & latlng back 
      this.viewCtrl.dismiss({ 
        description: item.description, 
        latitude: responses[0].geometry.location.lat(), 
        longitude: responses[0].geometry.location.lng() 
      }); 
    }); 
  } 

  updateSearch() { 
    if (this.autocomplete.query == '') { 
      this.autocompleteItems = []; 
      return; 
    } 
    let that = this; 
    this.service.getPlacePredictions({ 
      input: that.autocomplete.query, 
      componentRestrictions: { 
        country: 'IN' 
      } 
    }, (predictions, status) => { 
      that.autocompleteItems = []; 
      that.zone.run(function() { 
        predictions = predictions || []; 
        predictions.forEach(function(prediction) { 
          that.autocompleteItems.push(prediction); 
        }); 
      }); 
    }); 
  } 
}

在这里,我们使用google.maps.places.AutocompleteService来获取用户搜索时的预测。

重要的一点要注意的是,地点和地理编码器服务不作为 Ionic Native 插件提供。因此,我们将使用 Google Maps JavaScript 库来访问地点和地理编码器服务。为此,我们将安装 typings 和 Google Maps。我们将在最后安装这个。

用户找到地点后,他们将点击位置,这将触发chooseItem()。在chooseItem()内,我们将获取place_id并获取所选位置的地理坐标,并将其传递回BookRidePage类中selectDestination()内的modal.onDidDismiss()。然后流程就像我们在BookRidePage类中看到的那样。

现在,我们将实现profilehistorypaymentMethods端点。要生成所需的页面,请运行以下命令:

ionic generate page profile 
ionic generate page history 
ionic generate page paymentMethods

接下来,我们将同样添加到riderr/src/app/app.module.ts中。app.module.ts的最终版本将如下所示:

import { NgModule, ErrorHandler } from '@angular/core'; 
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular'; 
import { MyApp } from './app.component'; 
import { HomePage } from '../pages/home/home'; 
import { LoginPage } from '../pages/login/login'; 
import { BookRidePage } from '../pages/book-ride/book-ride'; 
import { AutocompletePage } from '../pages/auto-complete/auto-complete'; 
import { ProfilePage } from '../pages/profile/profile'; 
import { HistoryPage } from '../pages/history/history'; 
import { PaymentMethodsPage } from '../pages/payment-methods/payment-methods'; 

import { UberAPI } from '../services/uber.service'; 
import { Storage } from '@ionic/storage'; 

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

// export function provideStorage() { 
//   return new Storage();  
// } 

@NgModule({ 
  declarations: [ 
    MyApp, 
    HomePage, 
    LoginPage, 
    BookRidePage, 
    AutocompletePage, 
    ProfilePage, 
    HistoryPage, 
    PaymentMethodsPage 
  ], 
  imports: [ 
    IonicModule.forRoot(MyApp) 
  ], 
  bootstrap: [IonicApp], 
  entryComponents: [ 
    MyApp, 
    HomePage, 
    LoginPage, 
    BookRidePage, 
    AutocompletePage, 
    ProfilePage, 
    HistoryPage, 
    PaymentMethodsPage 
  ], 
  providers: [{ provide: ErrorHandler, useClass: IonicErrorHandler }, 
    UberAPI, 
    // {provide: Storage, useFactory: provideStorage}, 
    Storage, 
    StatusBar, 
    SplashScreen, 
    Diagnostic 
  ] 
}) 
export class AppModule { }

现在我们将更新我们已经搭建好的三个页面。这些页面中的几乎所有内容都相当容易理解。

riderr/src/pages/profile/profile.html中的 HTML 将如下所示:

<ion-header> 
    <ion-navbar>s 
        <button ion-button menuToggle> 
            <ion-icon name="menu"></ion-icon> 
        </button> 
        <ion-title>Riderr</ion-title> 
    </ion-navbar> 
</ion-header> 
<ion-content padding> 
    <h2 text-center>Your Profile</h2> 
    <hr> 
    <ion-list *ngIf="profile"> 
        <ion-item> 
            <ion-avatar item-left> 
                <img src="img/{{profile.picture}}"> 
            </ion-avatar> 
            <h2>{{profile.first_name}} {{profile.last_name}}</h2> 
            <h3>{{profile.email}}</h3> 
            <p>{{profile.promo_code}}</p> 
        </ion-item> 
    </ion-list> 
</ion-content>

riderr/src/pages/profile/profile.ts中所需的逻辑如下所示:

import { Component } from '@angular/core'; 
import { UberAPI } from '../../services/uber.service'; 

@Component({ 
  selector: 'page-profile', 
  templateUrl: 'profile.html' 
}) 
export class ProfilePage { 
  private profile; 
  constructor(private uberApi: UberAPI) { } 

  ngAfterViewInit() { 
    this.uberApi.getMe().subscribe((data) => { 
      // console.log(data.json()); 
      this.profile = data.json(); 
      // need a clean way to fix this! 
      this.uberApi.hideLoader(); 
    }, (err) => { 
      console.log(err); 
      this.uberApi.hideLoader(); 
    }); 
  } 
}

接下来,我们将处理HistoryPageriderr/src/pages/history/history.html中的 HTML 将如下所示:

<ion-header> 
    <ion-navbar> 
        <button ion-button menuToggle> 
            <ion-icon name="menu"></ion-icon> 
        </button> 
        <ion-title>Riderr</ion-title> 
    </ion-navbar> 
</ion-header> 
<ion-content padding> 
    <h2 text-center>Your Ride History</h2> 
    <hr> 
    <h3 text-center *ngIf="total">Showing last {{count}} of {{total}} rides</h3> 
    <ion-list> 
        <ion-item *ngFor="let h of history"> 
            <h2>{{ h.start_city.display_name }}</h2> 
            <h3>Completed at {{ h.end_time | date: 'hh:mm a'}}</h3> 
            <p>Distance : {{ h.distance }} Miles</p> 
        </ion-item> 
    </ion-list> 
</ion-content>

riderr/src/pages/history/history.ts中的相关逻辑如下所示:

import { Component } from '@angular/core'; 
import { UberAPI } from '../../services/uber.service'; 

@Component({ 
  selector: 'page-history', 
  templateUrl: 'history.html' 
}) 
export class HistoryPage { 
  history: Array<any>; 
  total: Number; 
  count: Number; 

  constructor(private uberApi: UberAPI) { } 

  ngAfterViewInit() { 
    this.uberApi.getHistory().subscribe((data) => { 
      // console.log(data.json()); 
      let d = data.json(); 
      this.history = d.history; 
      this.total = d.count; 
      this.count = d.history.length; 

      // need a clean way to fix this! 
      this.uberApi.hideLoader(); 
    }, (err) => { 
      console.log(err); 
      this.uberApi.hideLoader(); 
    }); 
  } 
}

最后,我们将实现支付方式。相同的 HTML 将在riderr/src/pages/payment-methods/payment-methods.html中如下所示:

<ion-header> 
    <ion-navbar> 
        <button ion-button menuToggle> 
            <ion-icon name="menu"></ion-icon> 
        </button> 
        <ion-title>Riderr</ion-title> 
    </ion-navbar> 
</ion-header> 
<ion-content padding> 
    <h2 text-center>Your Payment Methods</h2> 
    <hr> 
    <ion-list *ngIf="payment_methods"> 
        <ion-item *ngFor="let pm of payment_methods"> 
            <h2>{{ pm.type }}</h2> 
            <h3>{{ pm.description }}</h3> 
        </ion-item> 
    </ion-list> 
</ion-content>

riderr/src/pages/payment-methods/payment-methods.ts中所需的逻辑如下所示:

import { Component } from '@angular/core'; 
import { UberAPI } from '../../services/uber.service'; 

@Component({ 
  selector: 'page-payment-methods', 
  templateUrl: 'payment-methods.html' 
}) 
export class PaymentMethodsPage { 
  payment_methods; 

  constructor(private uberApi: UberAPI) { } 

  ngAfterViewInit() { 
    this.uberApi.getPaymentMethods().subscribe((data) => { 
      // console.log(data.json()); 
      this.payment_methods = data.json().payment_methods; 
      // need a clean way to fix this! 
      this.uberApi.hideLoader(); 
    }, (err) => { 
      console.log(err); 
      this.uberApi.hideLoader(); 
    }); 
  } 
}

有了这个,我们完成了所需的代码。接下来,我们将安装所需的插件和库。

安装依赖项

运行以下命令安装此应用所需的 Cordova 插件:

ionic plugin add cordova.plugins.diagnostic 
ionic plugin add cordova-plugin-geolocation 
ionic plugin add cordova-plugin-inappbrowser 
ionic plugin add cordova-sqlite-storage 
ionic plugin add cordova-custom-config

以及它们的 Ionic Native 模块:

npm install --save @ionic-native/google-maps 
npm install --save @ionic-native/Geolocation 
npm install --save @ionic-native/diagnostic 
npm install --save @ionic-native/in-app-browser 
npm install --save @ionic/storage

接下来,我们将安装 Google Maps 的 Cordova 插件。但在安装之前,我们需要获取一个 API 密钥。使用developers.google.com/maps/documentation/android-api/signup上的 Get A Key 按钮来启用 Android 应用的 Google Maps API 并获取一个密钥。对于 iOS,请转到以下页面:developers.google.com/maps/documentation/ios-sdk/get-api-key

获得 API 密钥后,运行以下命令:

ionic plugin add cordova-plugin-googlemaps --variable API_KEY_FOR_ANDROID=" AIzaSyCZhTJB1kFAP70RuwDtt6uso9e3DCLdRWs" --variable API_KEY_FOR_IOS="AIzaSyCZhTJB1kFAP70RuwDtt6uso9e3DCLdRWs"

注意:请使用您的密钥更新上述命令。

接下来,为了使用 Google Maps Places 服务,我们需要获取一个用于通过 JavaScript 访问地图服务的 API 密钥。转到developers.google.com/maps/documentation/JavaScript/get-api-key获取 JavaScript 的密钥。然后打开riderr/src/index.html并在文档的头部添加以下引用:

<script src="img/js?v=3&libraries=places&key=AIzaSyDmFpX80vy5p0YTuXGAgVJzWTkZfDqPl_s"></script>

接下来,为了让 TypeScript 编译器不对riderr/src/pages/auto-complete/auto-complete.ts中的google变量抱怨,我们需要添加所需的 typings。运行以下命令:

npm install typings --global

接下来,运行以下命令:

typings install dt~google.maps --global --save

打开riderr/tsconfig.json并将"typings/*.d.ts"添加到"include"数组中,如下所示:

{ 
  "compilerOptions": { 
    "allowSyntheticDefaultImports": true, 
    "declaration": false, 
    "emitDecoratorMetadata": true, 
    "experimentalDecorators": true, 
    "lib": [ 
      "dom", 
      "es2015" 
    ], 
    "module": "es2015", 
    "moduleResolution": "node", 
    "sourceMap": true, 
    "target": "es5" 
  }, 
  "include": [ 
    "src/**/*.ts", 
    "typings/*.d.ts" 
  ], 
  "exclude": [ 
    "node_modules" 
  ], 
  "compileOnSave": false, 
  "atom": { 
    "rewriteTsconfig": false 
  } 
}

有关如何安装 Google 地图的 TypeScript typings,请参阅:stackoverflow.com/a/40854384/1015046 获取更多信息。

最后,我们需要请求互联网访问和网络访问权限。打开riderr/config.xml并按照以下方式更新<platform name="android"></platform>

<platform name="android"> 
        <allow-intent href="market:*" /> 
        <config-file target="AndroidManifest.xml" parent="/*"> 
            <uses-permission android:name="android.permission.INTERNET" 
            /> 
            <uses-permission 
            android:name="android.permission.ACCESS_FINE_LOCATION" /> 
            <uses-permission 
            android:name="android.permission.ACCESS_COARSE_LOCATION" /> 
        </config-file> 
    </platform>

然后在页面顶部的 widget 标签中添加xmlns:android=http://schemas.android.com/apk/res/android,如下所示:

<widget id="app.example.riderr" version="0.0.1"   >

这就结束了安装依赖项部分。

测试应用

让我们继续测试该应用。首先,我们需要添加所需的平台。运行ionic platform add androidionic platform add ios

要测试该应用程序,我们需要模拟器或实际设备。

一旦设备/模拟器设置好,我们可以运行ionic run androidionic run ios命令。

流程如下:

首先,用户启动应用程序。将呈现登录屏幕,如下所示:

一旦用户点击“使用 Uber 登录”,我们将用户重定向到 Uber 授权屏幕,在那里用户将使用他们的 Uber 帐户登录:

认证成功后,将显示同意屏幕,并列出应用程序请求的权限列表:

一旦用户允许应用访问数据,我们将用户重定向到主页。

在主页上,我们提供了访问用户位置的同意弹出窗口:

一旦获得批准,我们将获得用户的地理位置,并使用该位置获取产品。

以下是完全加载的主屏幕截图:

菜单如下:

从这里,用户可以查看他们的个人资料:

他们可以查看他们的乘车历史:

他们还可以查看他们的付款方式:

在用户选择目的地之前,他们需要选择一个产品:

一旦他们选择了产品,他们可以选择要乘坐的目的地:

现在,我们制作车费明细并显示相同的内容:

如果用户同意,我们将预订乘车并显示乘车信息:

请注意应用程序右上角的取消乘车按钮。这将取消当前的乘车。

再次提醒,我们正在调用沙盒 API URL。如果您想请求实际乘车服务,请在riderr/src/services/uber.service.ts中将UBERSANDBOXAPIURL更新为UBERAPIURL

使用 Uber(生产)API 时,当我们请求乘车时,我们会收到处理响应。我们可以继续轮询几次以获取当前乘车信息。如果您发出实际乘车请求,响应将如下所示:

{ 
    "status": "accepted", 
    "product_id": "18ba4578-b11b-49a6-a992-a132f540b027", 
    "destination": { 
        "latitude": 17.445949, 
        "eta": 34, 
        "longitude": 78.350058 
    }, 
    "driver": { 
        "phone_number": "+910000000000", 
        "rating": 4.6, 
        "picture_url": 
        "https:\/\/d1w2poirtb3as9.cloudfront.net\
        /605de11c25139a1de469.jpeg", 
        "name": "John Doe", 
        "sms_number": null 
    }, 
    "pickup": { 
        "latitude": 17.4908514, 
        "eta": 13, 
        "longitude": 78.3375952 
    }, 
    "request_id": "1beaae05-8d43-4711-951c-25dd5293c2f9", 
    "location": { 
        "latitude": 17.4875583, 
        "bearing": 338, 
        "longitude": 78.33165 
    }, 
    "vehicle": { 
        "make": "Maruti Suzuki", 
        "picture_url": null, 
        "model": "Swift Dzire", 
        "license_plate": "XXXXXXXX" 
    }, 
    "shared": false 
}

您可以相应地构建您的界面。

摘要

在本章中,我们已经通过 Ionic 构建了一个应用,并将其与 Uber API 以及使用 Ionic Native 的设备功能集成。我们还使用了 Google Places Service 作为原始 JavaScript 库,并使用 typings 将其与我们的 Ionic 应用集成。

在下一章中,我们将看一下将 Ionic 1 应用迁移到 Ionic 2。如果您从 Ionic 1 迁移到 Ionic 3,这也适用。

第八章:Ionic 2 迁移指南

在本章中,我们将看看如何将现有的 Ionic 1 应用迁移到 Ionic 2/Ionic 3。我们首先将使用 Ionic 1 构建一个简单的 Todo 应用,然后了解如何将其迁移到 Ionic 2:

  • 为什么要迁移?

  • 构建一个简单的 Ionic 1 Todo 应用

  • 迁移计划

  • 将 Ionic 1 Todo 应用迁移到 Ionic 2

如果您想要从 Ionic 1 迁移到 Ionic 3,本迁移指南仍然有效。请参考第十一章,Ionic 3,以更好地了解 Ionic 3 中的变化。

为什么要迁移?

到目前为止,在这本书中,我们已经学习了使用 Ionic 2 构建应用的过程,但并不了解 Ionic 1。但就现实世界而言,已经有数千个应用程序使用了 Ionic 1 部署。这些应用可以利用 Ionic 2 的改进功能来提高应用体验。

在软件世界中迁移代码是一项艰巨的任务。在我们的情况下,迁移更加复杂,因为我们不仅要将 Ionic 1 的库升级到 Ionic 2,还要将这些库所写的语言本身升级,例如,从 ES5 到 ES6 和 TypeScript。

JavaScript 应用程序的新生态主要围绕 ES6、TypeScript 和 Web 组件展开。适应这些以利用最新技术就是 Angular 2 所做的。Ionic 2 也做到了这一点。

在我看来,有这么多的变化,将一个完全运行良好的应用程序从 Ionic 1 迁移到 Ionic 2 应该谨慎对待,只有在必要时才需要这样做。

如果事情顺利,为什么要改变呢?

当涉及将应用程序基础从 Ionic 1 更改为 Ionic 2 时,有些人称之为迁移,但我称之为重写。

Todo 应用 - Ionic v1

在本节中,我们将使用 Ionic 1 构建一个 Todo 应用。我们将构建的应用几乎包含了典型 Ionic 应用的所有特性。我们将拥有:

  • 路由

  • 持久性

  • 本地通知

  • REST API 集成

两页 Todo 应用中的第一页将是登录页面,第二页将是我们处理 Todos 的页面。我们将使用LocalStorage来保存认证状态以及我们将创建的 Todos。当用户创建、更新或删除待办事项时,我们还将显示本地通知。显示本地通知更多地是与设备功能进行接口的 Ionic 应用。最后,我们将发出对www.ipify.org/的 REST API 请求,以获取我们从中访问此应用的设备的 IP 地址。

最终应用程序将如下图所示:

构建应用程序

现在我们已经有了一个建设的想法,让我们开始吧。创建一个名为chapter8的文件夹,并在chapter8文件夹内打开一个新的命令提示符/终端并运行:

ionic start -a "TodoApp-v1" -i app.example.todoapp_v1 todoapp_v1  blank

我们正在使用 Ionic v1 搭建一个空白项目。请注意,我们没有使用--v2标志。一旦项目被搭建,就在你喜欢的文本编辑器中打开它。

在我们开始处理这两个页面之前,我们将创建所需的服务。我们将创建五个服务:

  • LocalStorage 服务:与 LocalStorage 进行交互

  • 本地通知服务:与$cordovaLocalNotification进行交互

  • IP 服务:与api.ipify.org进行交互

  • 认证服务:管理认证

  • Todos 服务:管理 Todos

为此,我们将在www/js文件夹内创建另一个名为services.js的文件。打开todoapp_v1/www/js/services.js并添加以下代码:

angular.module('starter') 
.service('LS', function($window) { // local storage 
    this.set = function(key, value) { 
        // http://stackoverflow.com/a/23656919/1015046 
        $window.localStorage.setItem(key, 
        $window.angular.toJson(value)); 
    } 

    this.get = function(key) { 
        return $window.JSON.parse($window.localStorage.getItem(key)); 
    } 

    this.remove = function(key) { 
        $window.localStorage.removeItem(key); 
    } 
})

LS服务公开了对 HTML5 localStorage的包装器。

接下来,在同一文件中为本地通知服务添加一个包装器,在LS服务之后:

// snipp 
.service('LN', function($ionicPlatform, $cordovaLocalNotification) { // local notifications 
    var i = 1; 
    this.show = function(text) { 
        $ionicPlatform.ready(function() { 
            var notifPromise = $cordovaLocalNotification.schedule({ 
                id: i++, 
                title: 'Todo App', 
                text: text 
            }) 
            return notifPromise; 
        }); 
    } 
})

在编写代码结束时,我们将从ngCordova添加所需的依赖项。

接下来,我们将添加IP服务以与api.ipify.org进行交互并获取用户的 IP 地址。追加以下代码:

// snipp 
.service('IP', function ($http) { 
    this.get = function(){ 
        return $http.get('https://api.ipify.org/?format=json'); 
    } 
})

最后,管理身份验证和待办事项的两个关键服务。添加以下代码:

// snipp 
.service('AUTH', function(LS) { 
    var LS_AUTH_KEY = 'auth'; 
    this.login = function(user) { 
        if (user.email === 'a@a.com', user.password === 'a') { 
            LS.set(LS_AUTH_KEY, true); 
            return true; 
        } else { 
            return false; 
        } 
    } 

    this.isAuthenticated = function() { 
        return !!LS.get(LS_AUTH_KEY); 
    } 

    this.logout = function() { 
        LS.remove(LS_AUTH_KEY); 
    } 

}) 

.service('TODOS', function(LS) { 
    var LS_TODOS_KEY = 'todos'; 

    this.set = function(todos) { 
        LS.set(LS_TODOS_KEY, todos); 
    } 

    this.get = function() { 
        return LS.get(LS_TODOS_KEY) || []; 
    } 
});

通过这样,我们已经完成了所需的服务。

由于这将是一个双页面应用程序,我们将使用 State 路由器来定义和管理路由。打开todoapp_v1/www/js/app.js并在run方法下添加以下config部分:

.config(function($stateProvider, $urlRouterProvider) { 
    $stateProvider 
        .state('login', { 
            url: '/login', 
            templateUrl: 'templates/login.html', 
            controller: 'LoginCtrl' 
        }) 
        .state('home', { 
            url: '/home', 
            templateUrl: 'templates/home.html', 
            controller: 'HomeCtrl' 
        }); 
    // if none of the above states are matched, use this as the fallback 
    $urlRouterProvider.otherwise('/login'); 
});

在上述片段中,我们定义了两个路由 - 登录和主页。现在我们需要创建所需的模板和控制器。

www/js文件夹中创建一个名为controllers.js的新文件。打开todoapp_v1/www/js/controllers.jsLoginCtrl,如下面的代码所示:

angular.module('starter') 

.controller('LoginCtrl', function($scope, AUTH, $state, $ionicHistory, $ionicPopup) { 

    // check Auth before proceeding 
    if (AUTH.isAuthenticated()) { 
        $state.go('home'); 
    } 

    // hardcode the test user 
    $scope.user = { 
        email: 'a@a.com', 
        password: 'a' 
    } 

    $scope.login = function() { 
        if (AUTH.login($scope.user)) { 
            // remove all views in stack 
            // this way when the user clicks on the  
            // back button on the home page 
            // we do not show the login screen again 
            $ionicHistory.clearHistory(); 
            $state.go('home'); 
        } else { 
            $ionicPopup.alert({ 
                title: 'LOGIN FAILED', 
                template: 'Either the email or password is invalid.' 
            }); 
        }; 
    } 
})

在这里,我们正在检查用户是否已经经过身份验证,如果是,我们将用户重定向到主页。login()接受用户的凭据并使用AUTH.login()验证它们。如果身份验证失败,我们将使用$ionicPopup服务显示警报。

接下来,我们将按照以下代码添加HomeCtrl

// snipp 

.controller('HomeCtrl', function($scope, $state, AUTH, TODOS, $ionicHistory, $ionicPopup, $ionicListDelegate, LN) { 

    $scope.todo = {}; 
    // check Auth before proceeding 
    if (!AUTH.isAuthenticated()) { 
        $state.go('login'); 
    } 

    // fetch todos on load 
    $scope.todos = TODOS.get(); 

    $scope.add = function() { 
        //reset 
        $scope.todo.text = ''; 
        var addTodoPopup = $ionicPopup.show({ 
            template: '<input type="text" ng-model="todo.text">', 
            title: 'Add Todo', 
            subTitle: 'Enter a Todo To Do', 
            scope: $scope, 
            buttons: [ 
                { text: 'Cancel' }, { 
                    text: '<b>Save</b>', 
                    type: 'button-positive', 
                    onTap: function(e) { 
                        // validation 
                        if (!$scope.todo.text) { 
                            e.preventDefault(); 
                        } else { 
                            return $scope.todo.text; 
                        } 
                    } 
                } 
            ] 
        }); 

        addTodoPopup.then(function(text) { 
            if (text) { 
                var todo = { 
                    text: text, 
                    isCompleted: false 
                }; 

                $scope.todos.push(todo); 
                // save it to LS 
                TODOS.set($scope.todos); 
                LN.show('Todo Created'); 
            } 
        }); 
    } 

    $scope.update = function(todo) { 
        todo.isCompleted = !todo.isCompleted; 
        $ionicListDelegate.closeOptionButtons(); 
        // update LS 
        TODOS.set($scope.todos); 
        LN.show('Todo Updated'); 
    } 

    $scope.delete = function($index, todo) { 

        var deleteConfirmPopup = $ionicPopup.confirm({ 
            title: 'Delete Todo', 
            template: 'Are you sure you want to delete "' + todo.text + 
            '"? ' 
        }); 

        deleteConfirmPopup.then(function(res) { 
            if (res) { 
                $scope.todos.splice($index, 1); 
                // update LS 
                TODOS.set($scope.todos); 
                LN.show('Todo Deleted'); 
            } 
        }); 
    } 

    $scope.logout = function() { 
        AUTH.logout(); 
        $ionicHistory.clearHistory(); 
        $state.go('login'); 
    } 
});

我们首先检查身份验证。接下来,我们获取所有的待办事项。我们在HomeCtrl范围上定义了四种方法:add()update()delete()logout()

添加方法用于添加新的待办事项。我们使用$ionicPopup服务显示一个弹出窗口,用户在其中输入待办事项文本。一旦待办事项被添加,我们使用LN服务推送一个本地通知。

更新方法在本地存储中更新待办事项的isCompleted属性,并推送一个指示相同内容的本地通知。

删除方法显示一个确认框,询问用户确认删除操作。如果用户确认删除,我们将从集合中删除待办事项并将集合持久化到本地存储中。为了完成删除过程,我们推送一个本地通知指示待办事项已被删除。

最后,注销方法清除身份验证状态并将用户重定向回登录页面。

现在我们已经完成了控制器,我们将开始处理所需的模板。在www文件夹中创建一个名为templates的新文件夹。在模板文件夹中,创建一个名为login.html的文件。打开todoapp_v1/www/templates/login.html并按照以下代码进行更新:

<ion-view view-> 
    <ion-content> 
        <div class="list"> 
            <label class="item item-input"> 
                <span class="input-label">Username</span> 
                <input type="email" ng-model="user.email" 
                placeholder="Enter your email"> 
            </label> 
            <label class="item item-input"> 
                <span class="input-label">Password</span> 
                <input type="password" ng-model="user.password" 
                placeholder="Enter your password"> 
            </label> 
            <button ng-click="login()" class="button button-positive 
            button-full" ng-disabled="!user.email || !user.password"> 
                Login 
            </button> 
        </div> 
    </ion-content> 
    <ion-footer-bar align- class="bar-positive"> 
        <h1 class="title">Your IP : {{ip}}</h1> 
    </ion-footer-bar> 
</ion-view>

我们有一个简单的登录表单。在页脚中,我们将显示用户的 IP 地址。为了获取用户的 IP 地址,我们将按照以下代码更新todoapp_v1/www/js/app.js中的run方法:

// snipp 
.run(function($ionicPlatform, IP, $rootScope) { 
    $ionicPlatform.ready(function() { 
        if (window.cordova && window.cordova.plugins.Keyboard) { 
            cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true); 
            cordova.plugins.Keyboard.disableScroll(true); 
        } 
        if (window.StatusBar) { 
            StatusBar.styleDefault(); 
        } 

        IP.get().then(function(resp) { 
            // console.log(resp.data); 
            $rootScope.ip = resp.data.ip; 
        }); 
    }); 
}) 
// snipp

我们将 IP 地址存储在根作用域上。

接下来,在www/templates文件夹中创建一个名为home.html的新文件。按照以下代码更新todoapp_v1/www/templates/home.html

<ion-view view-> 
    <ion-nav-bar class="bar-default"> 
        <ion-nav-buttons side="right"> 
            <button class="button button-assertive" ng-click=" 
            logout()"> 
                Logout 
            </button> 
        </ion-nav-buttons> 
    </ion-nav-bar> 
    <ion-content> 
        <ion-list can-swipe="true"> 
            <ion-item> 
                <button class="button button-full button-positive" ng-
                click="add()"> 
                    Add Todo 
                </button> 
            </ion-item> 
            <ion-item ng-repeat="todo in todos"> 
                <h2 ng-class="{ 'strike' : todo.isCompleted}">
                {{todo.text}}</h2> 
                <ion-option-button class="button-assertive icon ion-
                trash-a" ng-click="delete($index, todo)">
                </ion-option-button> 
                <ion-option-button class="button-positive icon" ng-               
                class="{'ion-checkmark-round' : 
                !todo.isCompleted, 'ion-close-round' :
                todo.isCompleted}" ng-click="update(todo)">
                </ion-option-button> 
            </ion-item> 
            <ion-item ng-if="todos.length > 0"> 
                <p class="text-center">Swipe left for options</p> 
            </ion-item> 
            <ion-item ng-if="todos.length === 0"> 
                <h2 class="text-center">No Todos</h2> 
            </ion-item> 
        </ion-list> 
    </ion-content> 
    <ion-footer-bar align- class="bar-positive"> 
        <h1 class="title">Your IP : {{ip}}</h1> 
    </ion-footer-bar> 
</ion-view>

当用户标记todo为已完成时,为了视觉效果,我们添加了一个名为strike的类。打开todoapp_v1/www/css/style.css并按照以下代码进行更新:

.strike{ 
  text-decoration: line-through; 
  color: #999; 
}

通过这样,我们已经完成了实现所需代码。现在,我们将添加所需的依赖项并更新www/index.html

首先,我们将为我们的项目添加ngCordova(ngcordova.com/)支持。运行以下命令:

bower install ngCordova --save

接下来是本地通知插件:(ngcordova.com/docs/plugins/localNotification/) cordova plugin add: github.com/katzer/cordova-plugin-local-notifications.git

现在,我们将更新www/index.html以添加ngCordova依赖项。添加以下内容:

<script src="img/ng-cordova.js"></script> before <script src="img/cordova.js"></script>.

接下来,添加对services.jscontrollers.js的引用:

<script src="img/services.js"></script> 
<script src="img/controllers.js"></script>

app.js已经被包含之后。接下来,将按照以下代码更新 body 部分:

<ion-pane> 
        <ion-nav-bar class="bar-positive"> 
        </ion-nav-bar> 
        <ion-nav-view></ion-nav-view> 
</ion-pane>

我们已经添加了<ion-nav-view></ion-nav-view>以支持路由。

现在,打开todoapp_v1/www/js/app.js并更新启动模块定义为:angular.module('starter', ['ionic', 'ngCordova'])

就是这样!现在我们需要做的就是添加一个平台并开始测试使用 Ionic v1 构建的待办事项应用程序:

ionic platform add android or ionic platform add ios

然后运行以下命令:

ionic run android or ionic run ios

然后我们应该看到登录页面出现:

成功登录后,我们应该能够添加新的待办事项:

我们可以更新待办事项或删除待办事项:

当添加、更新或删除待办事项时,我们会推送本地通知:

通过这样,我们完成了构建 Ionic 1 待办事项应用程序。

迁移计划

现在我们完成了 Ionic v1 待办事项应用程序,我们将开始考虑将其迁移到 Ionic 2。

注意:如果您计划从 Ionic 1 迁移到 Ionic 3,您将遵循类似的方法。

计划很简单;我们将使用--v2标志搭建一个新的空白模板,并开始组合东西。以下表格将是一个很好的起点:

组件 Ionic 1 Ionic 2
Ionic 起始模板 空白 空白
引导应用程序 ng-app NgModule
导航 状态路由器 NavController
组件 模板和控制器 @Component
服务/工厂 服务提供者 @Injectable Provider
持久性 本地存储 Storage API
设备交互 NgCordova Ionic Native
本地通知 $cordovaLocalNotification服务 LocalNotifications 类

现在我们知道了高级映射,我们将从头开始在 v2 中搭建一个新的空白模板。

chapter8文件夹中,打开一个新的命令提示符/终端并运行:

ionic start -a "TodoApp-v2" -i app.example.todoapp_v2 todoapp_v2  blank --v2

完成搭建后,cd进入todoapp_v2文件夹。我们将生成所需的组件和提供者。运行以下命令:

 ionic generate page login

这将生成登录页面。接下来,三个提供者:

ionic generate provider auth 
ionic generate provider todos 
ionic generate provider IP

由于我们在 Ionic 2 中使用了 Storage API,我们不会为此创建单独的提供者。

现在我们有了所需的页面和提供者,我们将引导应用程序。

打开todoapp_v2/src/app/app.module.ts并进行所需的导入:

// snipp 
import { LoginPage } from '../pages/login/login'; 

import { Auth } from '../providers/auth'; 
import { Todos } from '../providers/todos'; 
import { IP } from '../providers/ip'; 

import { IonicStorageModule } from '@ionic/storage'; 
import { LocalNotifications } from '@ionic-native/local-notifications';

接下来,我们将按照以下代码更新@NgModule

@NgModule({ 
  declarations: [ 
    MyApp, 
    HomePage, 
    LoginPage 
  ], 
  imports: [ 
    IonicModule.forRoot(MyApp), 
    IonicStorageModule.forRoot() 
  ], 
  bootstrap: [IonicApp], 
  entryComponents: [ 
    MyApp, 
    HomePage, 
    LoginPage 
  ], 
  providers: [ 
    StatusBar, 
    SplashScreen, 
    {provide: ErrorHandler, useClass: IonicErrorHandler}, 
    Auth, 
    Todos, 
    IP, 
    LocalNotifications 
  ] 
})

就像我们在 Ionic 1 应用程序中所做的那样,我们将在最后安装所需的依赖项。

打开todoapp_v2/src/app/app.component.ts并将rootPage更新为LoginPage。我们将从'../pages/home/home';更新import { HomePage }import { LoginPage }rootPage = HomePage;rootPage = LoginPage;

现在,我们将更新提供者。打开todoapp_v2/src/providers/ip.ts并按照以下代码进行更新:

import { Injectable } from '@angular/core'; 
import { Http, Response } from '@angular/http'; 
import { Observable } from 'rxjs/Observable'; 

@Injectable() 
export class IP { 
  constructor(private http: Http) {} 

  get() : Observable <Response>{ 
    return this.http.get('https://api.ipify.org/?format=json'); 
  } 
}

接下来,打开todoapp_v2/src/providers/auth.ts。按照以下内容进行更新:

import { Injectable } from '@angular/core'; 
import { Storage } from '@ionic/storage'; 

@Injectable() 
export class Todos { 
  private LS_TODOS_KEY = 'todos'; 

  constructor(private storage: Storage) { } 

  set(todos): void { 
    this.storage.set(this.LS_TODOS_KEY, todos); 
  } 

  get(): Promise<any> { 
    return this.storage.get(this.LS_TODOS_KEY); 
  } 
}

最后,打开todoapp_v2/src/providers/auth.ts并按照以下内容进行更新:

import { Injectable } from '@angular/core'; 
import { Storage } from '@ionic/storage'; 

@Injectable() 
export class Auth { 
  private LS_AUTH_KEY = 'auth'; 

  constructor(private storage: Storage) { } 

  login(user: any): Boolean { 
    if (user.email === 'a@a.com', user.password === 'a') { 
      this.storage.set(this.LS_AUTH_KEY, true) 
      return true; 
    } else { 
      return false; 
    } 
  } 

  isAuthenticated(): Promise<Storage> { 
    return this.storage.get(this.LS_AUTH_KEY); 
  } 

  logout(): void { 
    this.storage.set(this.LS_AUTH_KEY, undefined); 
  } 
}

前面的三个提供者非常简单。它们复制了 Ionic 1 中所示的相同逻辑,只是这些是用 TypeScript 编写的,遵循 Angular 2 的结构。

现在,我们将在页面上进行工作。首先是登录页面。打开todoapp_v2/src/pages/login/login.ts并按照以下代码进行更新:

import { Component } from '@angular/core'; 
import { NavController, AlertController } from 'ionic-angular'; 
import { HomePage } from '../home/home'; 
import { Auth } from '../../providers/auth'; 
import { IP } from '../../providers/ip'; 

@Component({ 
  selector: 'page-login', 
  templateUrl: 'login.html' 
}) 
export class LoginPage { 
  userIp = ''; 
  user = { 
    email: 'a@a.com', 
    password: 'a' 
  } 

  constructor( 
    public navCtrl: NavController, 
    public alertCtrl: AlertController, 
    private auth: Auth, 
    private ip: IP) { 

    // check if the user is already  
    // authenticated 
    auth.isAuthenticated().then((isAuth) => { 
      if (isAuth) { 
        navCtrl.setRoot(HomePage); 
      } 
    }); 

    // Get the user's IP 
    ip.get().subscribe((data) => { 
      this.userIp = data.json().ip; 
    }); 
  } 

  login() { 
    if (this.auth.login(this.user)) { 
      this.navCtrl.setRoot(HomePage); 
    } else { 
      let alert = this.alertCtrl.create({ 
        title: 'LOGIN FAILED', 
        subTitle: 'Either the email or password is invalid.', 
        buttons: ['OK'] 
      }); 
      alert.present(); 
    } 
  } 
}

这个文件中的逻辑与 Ionic 1 应用程序中的LoginCtrl的逻辑非常相似。接下来,我们将按照以下代码更新todoapp_v2/src/pages/login/login.html

<ion-header class="positive"> 
    <ion-navbar> 
        <ion-title>Todo App (v2)</ion-title> 
    </ion-navbar> 
</ion-header> 
<ion-content> 
    <ion-list> 
        <ion-item> 
            <ion-label fixed>Username</ion-label> 
            <ion-input type="email" placeholder="Enter your email" 
            [(ngModel)]="user.email"></ion-input> 
        </ion-item> 
        <ion-item> 
            <ion-label fixed>Password</ion-label> 
            <ion-input type="password" placeholder="Enter your 
            password" [(ngModel)]="user.password"></ion-input> 
        </ion-item> 
    </ion-list> 
    <button ion-button full (click)="login()" [disabled]="!user.email || !user.password">Login</button> 
</ion-content> 
<ion-footer>
  <h3>Your IP : {{userIp}}</h3>
</ion-footer>

页面结构与 Ionic 1 完全相同,只是我们与组件交互的方式不同;[(ngModel)]语法用于双向数据绑定(ng-model)(click)语法用于按钮上的事件处理(ng-click).

请注意ion-header上的 positive 类。我们将使用这个类来为页面提供几乎相同的外观和感觉,就像我们在 Ionic 1 应用程序中所做的那样。

现在我们将在todoapp_v2/src/pages/home/home.ts上进行工作。按照以下代码更新todoapp_v2/src/pages/home/home.ts

import { Component } from '@angular/core'; 
import { LocalNotifications } from '@ionic-native/local-notifications'; 
import { NavController, AlertController } from 'ionic-angular'; 
import { LoginPage } from '../login/login'; 
import { Auth } from '../../providers/auth'; 
import { IP } from '../../providers/ip'; 
import { Todos } from '../../providers/todos'; 

@Component({ 
  selector: 'page-home', 
  templateUrl: 'home.html' 
}) 
export class HomePage { 
  private i = 1; // ID for notifications 
  userIp = ''; 
  userTodos = []; 

  constructor( 
    public navCtrl: NavController, 
    public alertCtrl: AlertController, 
    private localNotifications: LocalNotifications, 
    private auth: Auth, 
    private ip: IP, 
    private todos: Todos) { 

    // check if the user is authenticated 
    auth.isAuthenticated().then((isAuth) => { 
      if (!isAuth) { 
        navCtrl.setRoot(LoginPage); 
      } 
    }); 

    // fetch todos on load 
    this.todos.get().then((_todos) => { 
      this.userTodos = _todos || []; 
    }); 

    // Get the user's IP 
    ip.get().subscribe((data) => { 
      this.userIp = data.json().ip; 
    }); 
  } 

  add() { 
    let addTodoPopup = this.alertCtrl.create({ 
      title: 'Add Todo', 
      inputs: [ 
        { 
          name: 'text', 
          placeholder: 'Enter a Todo To Do' 
        } 
      ], 
      buttons: [ 
        { 
          text: 'Cancel', 
          role: 'cancel', 
          handler: (data) => { 
            // console.log('Cancel clicked'); 
          } 
        }, 
        { 
          text: 'Save', 
          handler: (data) => { 
            if (data.text) { 
              let todo = { 
                text: data.text, 
                isCompleted: false 
              }; 
              this.userTodos.push(todo); 
              // store the todos 
              this.todos.set(this.userTodos); 
              this.notify('Todo Created'); 

            } else { 
              return false; 
            } 
          } 
        } 

      ] 
    }); 
    addTodoPopup.present(); 
  } 

  update(todo, slidingItem) { 
    todo.isCompleted = !todo.isCompleted; 
    // store the todos 
    this.todos.set(this.userTodos); 
    slidingItem.close(); 
    this.notify('Todo Updated'); 
  } 

  delete(todo, index) { 
    let alert = this.alertCtrl.create({ 
      title: 'Delete Todo', 
      message: 'Are you sure you want to delete "' + todo.text + '"? ', 
      buttons: [ 
        { 
          text: 'No', 
          role: 'cancel', 
          handler: () => { 
            // console.log('Cancel clicked'); 
          } 
        }, 
        { 
          text: 'Yes', 
          handler: () => { 
            this.userTodos.splice(index, 1); 
            this.todos.set(this.userTodos); 
            this.notify('Todo Deleted'); 
          } 
        } 
      ] 
    }); 
    alert.present(); 

  } 

  logout() { 
    this.auth.logout(); 
    this.navCtrl.setRoot(LoginPage); 
  } 

  private notify(text) { 
    this.localNotifications.schedule({ 
      id: this.i++, 
      title: 'Todo App', 
      text: text, 
    }); 
  } 
}

这里复制了HomeCtrl的相同逻辑。唯一的关键区别是notify()被用作包装器来呈现本地通知,不像在 Ionic 1 应用程序中,我们为此使用了一个服务。

更新后的 todoapp_v2/src/pages/home/home.html 如下所示:

<ion-header> 
    <ion-navbar> 
        <ion-title>Todo App (v2)</ion-title> 
        <ion-buttons end> 
            <button ion-button color="danger" (click)="logout()"> 
                Logout 
            </button> 
        </ion-buttons> 
    </ion-navbar> 
</ion-header> 
<ion-content> 
    <button ion-button full (click)="add()"> 
        Add Todo 
    </button> 
    <ion-list can-swipe="true"> 
        <ion-item-sliding *ngFor="let todo of userTodos" #slidingItem> 
            <ion-item [class.strike]="todo.isCompleted"> 
                {{todo.text}} 
            </ion-item> 
            <ion-item-options side="right"> 
                <button ion-button icon-only (click)="update(todo, 
                slidingItem)"> 
                    <ion-icon [name]="todo.isCompleted ? 'close' : 
                    'checkmark'"></ion-icon> 
                </button> 
                <button ion-button icon-only color="danger" 
                (click)="delete(todo, index)"> 
                    <ion-icon name="trash"></ion-icon> 
                </button> 
            </ion-item-options> 
        </ion-item-sliding> 
        <ion-item *ngIf="userTodos.length > 0"> 
            <p text-center>Swipe left for options</p> 
        </ion-item> 
        <ion-item *ngIf="userTodos.length === 0"> 
            <h2 class="text-center">No Todos</h2> 
        </ion-item> 
    </ion-list> 
</ion-content> 
<ion-footer> 
    <h3>Your IP : {{userIp}}</h3> 
</ion-footer>

最后是样式。打开 todoapp_v2/src/app/app.scss 并添加以下 CSS 规则:

ion-header.positive ion-navbar .toolbar-background, 
ion-footer, 
{ 
    background-color: #387ef5; 
} 

ion-header.positive .toolbar-title, 
ion-footer { 
    color: #fff; 
} 

.toolbar-title, 
ion-footer { 
    text-align: center; 
} 

ion-navbar button[color=danger]{ 
    background: #f53d3d; 
    color: #fff; 
    border-radius: 4px 
} 

.strike { 
    text-decoration: line-through; 
    color:#999; 
}

这结束了我们的编码部分。现在,我们将安装所需的依赖项。首先是与存储相关的依赖项,运行以下命令:

ionic plugin add cordova-sqlite-storage -save 
npm install --save @ionic/storage

接下来是本地通知的依赖项:

ionic plugin add de.appplant.cordova.plugin.local-notification 
npm install --save @ionic-native/local-notifications

这应该满足所需的依赖关系。

现在,我们将添加一个平台并测试应用程序:

ionic platform add android or ionic platform add ios

然后运行以下命令:

ionic run android or ionic run ios

然后您应该看到登录页面弹出:

管理待办事项的主页:

最后是推送的通知:

通过这个,我们已经完成了将我们的 Ionic 1 Todo 应用程序迁移到 Ionic 2。希望这个例子给出了一些关于如何将 Ionic 1 应用程序迁移到 Ionic 2 以及 Ionic 3 的想法。

摘要

在本章中,我们已经了解了构建一个简单的 Ionic 1 Todo 应用程序的过程。接下来,我们准备了一个粗略的迁移计划,并按照相同的计划将 Ionic 1 Todo 应用程序迁移到 Ionic 2。我们已经看到了在迁移和利用最新功能(如 Ionic Native 和 Storage API)方面,Ionic 1 和 Ionic 2 应用程序之间的一些关键区别。

请查看第十一章,Ionic 3,以了解 Ionic 2 和 Ionic 3 之间的区别。

在下一章中,我们将测试我们迁移的 Ionic 2 Todo 应用程序。

第九章:测试 Ionic 2 应用

在本章中,我们将讨论如何测试使用 Cordova(和 Ionic 2)构建的移动混合应用。测试可以在多个层面进行,首先是单元测试,然后是端到端测试,最后将应用部署到实际设备上并执行测试。在本章中,我们将对我们在第八章中构建的 Ionic 2 Todo 应用执行以下测试:

  • 单元测试

  • 端到端测试

  • 使用 AWS 设备农场进行猴子或模糊测试

  • 使用 AWS 设备农场进行测试

测试方法学

在应用开发领域,测试进入应用开发生命周期的两种方式。一种是更传统的方式,其中首先进行开发,然后根据要求设计和执行测试运行。另一种更有效的方式是采用测试驱动开发TDD)。经过一段时间的验证,TDD 已被证明是一种更无缺陷的应用开发方式。您可以在这里阅读更多关于 TDD 的信息:agiledata.org/essays/tdd.html

TDD 的副产品是行为驱动测试BDT)。BDT 更多地围绕行为测试而不是需求测试。单元测试和 BDT 的自动化测试的良好组合将产生一个具有最小错误的优秀产品。由于 BDT 涉及更多以用户为中心的测试,因此可以在测试阶段轻松发现最终用户可能在测试阶段遇到的问题。

在本章中,我们将遵循测试应用的更传统流程,即在构建后进行测试。我们将实施单元测试、端到端测试,然后将应用上传到 AWS 设备农场并进行猴子测试。

设置单元测试环境

Ionic CLI 构建的应用在撰写本章的当天不包括任何测试设置。因此,我们需要自己添加所需的测试设置。

设置项目

首先,我们将创建一个名为chapter9的新文件夹,并将chapter8文件夹中的todoapp_v2复制到chapter9文件夹中。

通过从chapter9/todoapp_v2文件夹的根目录运行npm install来安装依赖项(如果缺少)。

运行ionic serve,查看应用是否按预期工作。当您创建、更新和删除todo时,您可能会在控制台中看到警告,指出 Cordova 环境不存在。这是因为我们在浏览器中使用本地通知插件。

我们将为单元测试我们的 Todo 应用进行环境设置,该设置基于文章:Ionic 2 Unit Testing Setup: The Best Way (www.roblouie.com/article/376/ionic-2-set-up-unit-testing-the-best-way/)。

要开始,我们将安装 Karma 和 Jasmine:

  • Karma:Karma 是一个在 Node.js 上运行的 JavaScript 测试运行器。引用 Karma 的文档,Karma 本质上是一个工具,它生成一个 Web 服务器,针对连接的每个浏览器执行源代码与测试代码。对每个浏览器的每个测试的结果进行检查,并通过命令行显示给开发人员,以便他们可以看到哪些浏览器和测试通过或失败。

我们将使用 Karma 来执行我们将要编写的测试用例:

  • Jasmine:Jasmine 是一个用于测试 JavaScript 代码的行为驱动开发框架。它不依赖于任何其他 JavaScript 框架。它不需要 DOM。它具有清晰明了的语法,因此我们可以轻松编写测试。

我们将使用 Jasmine 来定义我们的测试并编写断言。通常我们会通过编写一个描述块来开始测试。然后我们开始使用it构造定义我们的测试用例。

例如:

describe('Component: MyApp Component', () => { 
  it('should be created', () => { 
     // assertions go here 
  }); 
});

断言是简单的比较语句,用于验证实际结果和期望结果:

expect(1 + 1).toBe(2); 
expect(!!true).toBeTruthy();

依此类推。

现在我们对 Karma 和 Jasmine 有了基本的了解,我们将安装所需的依赖项。

在安装过程中,如果出现任何错误,请更新到最新版本的 Node.js。

要安装 Karma,请运行以下命令:

npm install -g karma-cli

接下来,安装 Jasmine 和相关依赖项:

npm install --save-dev @types/jasmine@2.5.41 @types/node html-loader jasmine karma karma-webpack ts-loader karma-sourcemap-loader karma-jasmine karma-jasmine-html-reporter angular2-template-loader karma-chrome-launcher null-loader karma-htmlfile-reporter

完成后,我们将添加所需的配置文件。

todoapp_v2文件夹的根目录下创建一个名为test-config的新文件夹。在test-config文件夹内,创建一个名为webpack.test.js的文件。使用以下代码更新todoapp_v2/test-config/webpack.test.js

var webpack = require('webpack'); 
var path = require('path'); 

module.exports = {
    devtool: 'inline-source-map',
    resolve: {
        extensions: ['.ts', '.js']
    },
    module: {
        rules: [{
            test: /.ts$/,
            loaders: [{
                loader: 'ts-loader'
            }, 'angular2-template-loader']
        }, {
            test: /.html$/,
            loader: 'html-loader'
        }, {
            test: /.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/,
            loader: 'null-loader'
        }]
    },
    plugins: [
        new webpack.ContextReplacementPlugin(
            // The (|/) piece accounts for 
            path separators in *nix and Windows
            /angular(|/)core(|/)
            (esm(|/)src|src)(|/)linker/,
            root('./src'), // location of your src
            {} // a map of your routes
        )
    ]
};

function root(localPath) { 
    return path.resolve(__dirname, localPath); 
}

接下来,在test-config文件夹内创建另一个名为karma-test-shim.js的文件。使用以下代码更新todoapp_v2/test-config/karma-test-shim.js

Error.stackTraceLimit = Infinity; 

require('core-js/es6'); 
require('core-js/es7/reflect'); 

require('zone.js/dist/zone'); 
require('zone.js/dist/long-stack-trace-zone'); 
require('zone.js/dist/proxy'); 
require('zone.js/dist/sync-test'); 
require('zone.js/dist/jasmine-patch'); 
require('zone.js/dist/async-test'); 
require('zone.js/dist/fake-async-test'); 

var appContext = require.context('../src', true, /.spec.ts/); 

appContext.keys().forEach(appContext); 

var testing = require('@angular/core/testing'); 
var browser = require('@angular/platform-browser-dynamic/testing'); 

testing.TestBed.initTestEnvironment(browser.BrowserDynamicTestingModule, browser.platformBrowserDynamicTesting());

最后,在test-config文件夹内创建一个名为karma.conf.js的文件。使用以下代码更新todoapp_v2/test-config/karma.conf.js

var webpackConfig = require('./webpack.test.js'); 
module.exports = function(config) { 
    var _config = { 
        basePath: '', 
        frameworks: ['jasmine'], 
        files: [ 
            { pattern: './karma-test-shim.js', watched: true } 
        ], 
        preprocessors: { 
            './karma-test-shim.js': ['webpack', 'sourcemap'] 
        }, 
        webpack: webpackConfig, 
        webpackMiddleware: { 
            stats: 'errors-only' 
        }, 
        webpackServer: { 
            noInfo: true 
        }, 
        reporters: ['html', 'dots'], 
        htmlReporter: { 
            outputFile: './unit-test-report.html', 
            pageTitle: 'Todo App Unit Tests', 
            subPageTitle: 'Todo App Unit Tests Report', 
            groupSuites: true, 
            useCompactStyle: true, 
            useLegacyStyle: true 
        }, 
        port: 9876, 
        colors: true, 
        logLevel: config.LOG_INFO, 
        autoWatch: true, 
        browsers: ['Chrome'], 
        singleRun: true 
    }; 
    config.set(_config); 
};

有了这些,我们完成了运行单元测试所需的基本配置。

前面提到的文章本身包含了我们添加的三个配置文件的所需信息。有关更多信息,请参阅:angular.io/docs/ts/latest/guide/webpack.html#!#test-configuration

编写单元测试

现在我们已经完成了所需的设置,我们将开始编写单元测试。单元测试写在与源文件相邻的文件中,文件名后面加上.spec。例如,如果我们为app.component.ts编写测试用例,我们将在相同的文件夹中创建一个名为app.component.spec.ts的文件,并编写所需的测试用例。

有关更多信息,请参阅angular.io/docs/ts/latest/guide/testing.html#!#q-spec-file-locationangular.io/docs/ts/latest/guide/style-guide.html#!#02-10

首先,我们将开始编写应用组件的测试。我们将测试以下情况:

  • 如果组件已创建。

  • 如果rootPage设置为LoginPage

现在,在todoapp_v2/src/app文件夹内创建一个名为app.component.spec.ts的文件。使用以下代码更新todoapp_v2/src/app/app.component.spec.ts

import { async, TestBed } from '@angular/core/testing'; 
import { IonicModule } from 'ionic-angular'; 
import { StatusBar } from '@ionic-native/status-bar'; 
import { SplashScreen } from '@ionic-native/splash-screen'; 
import { MyApp } from './app.component'; 
import { LoginPage } from '../pages/login/login'; 

describe('Component: MyApp Component', () => { 
  let fixture; 
  let component; 

  beforeEach(async(() => { 
    TestBed.configureTestingModule({ 
      declarations: [MyApp], 
      imports: [ 
        IonicModule.forRoot(MyApp) 
      ], 
      providers: [ 
        StatusBar, 
        SplashScreen 
      ] 
    }) 
  })); 

  beforeEach(() => { 
    fixture = TestBed.createComponent(MyApp); 
    component = fixture.componentInstance; 
  }); 

  it('should be created', () => { 
    expect(component instanceof MyApp).toBe(true); 
  }); 

  it('should set the rootPage as LoginPage', () => { 
    expect(component.rootPage).toBe(LoginPage); 
  }); 

});

有很多事情要做。首先,我们导入了所需的依赖项。接下来,我们添加了描述块。在描述块内,我们添加了beforeEach()beforeEach()在每次测试执行之前运行。在第一个beforeEach()中,我们定义了TestBed。在第二个beforeEach()中,我们创建了所需的组件并获取了它的实例。

TestBed配置和初始化了单元测试的环境。要深入了解 Angular 2 中的测试设置和执行方式,请查看:Testing Angular 2, Julie Ralph,网址:www.youtube.com/watch?v=f493Xf0F2yU

一旦TestBed被定义并且组件被初始化,我们就编写我们的测试用例。

注意:我们已经用async包装了beforeEach()的回调函数。async不会让下一个测试开始,直到所有待处理的任务都完成。要了解何时在测试中使用async,请参考Angular 2 Testing -- Async function call --when to usestackoverflow.com/a/40127164/1015046

接下来,我们将测试登录页面。

todoapp_v2/src/pages/login文件夹内创建一个名为login.spec.ts的文件。我们将测试以下内容:

  • 组件已创建

  • userIp变量被初始化为空字符串。

  • 用户对象包含值为a@a.com的电子邮件

  • 用户对象包含值为a的密码

使用以下代码更新todoapp_v2/src/pages/login/login.spec.ts

import { async, TestBed } from '@angular/core/testing'; 
import { IonicModule, NavController, AlertController } from 'ionic-angular'; 
import { IonicStorageModule } from '@ionic/storage'; 
import { MyApp } from '../../app/app.component'; 
import { LoginPage } from './login'; 
import { Auth } from '../../providers/auth'; 
import { IP } from '../../providers/ip'; 

describe('Component: Login Component', () => { 
  let fixture; 
  let component; 

  beforeEach(async(() => { 
    TestBed.configureTestingModule({ 
      declarations: [ 
        MyApp, 
        LoginPage 
      ], 
      imports: [ 
        IonicModule.forRoot(MyApp), 
        IonicStorageModule.forRoot() 
      ], 
      providers: [ 
        Auth, 
        IP, 
        NavController, 
        AlertController 
      ] 
    }) 
  })); 

  beforeEach(() => { 
    fixture = TestBed.createComponent(LoginPage); 
    component = fixture.componentInstance; 
  }); 

  it('should be created', () => { 
    expect(component instanceof LoginPage).toBe(true); 
  }); 

  it('should initialize `userIp` to ''', () => { 
    expect(component.userIp).toBe(''); 
  }); 

  it('should initialize `user`', () => { 
    expect(component.user.email).toBe('a@a.com'); 
    expect(component.user.password).toBe('a'); 
  }); 

});

上述代码相当容易理解。

接下来,我们转向主页组件。在todoapp_v2/src/pages/home文件夹内创建一个名为home.spec.ts的文件。在这个组件中,我们将测试以下内容:

  • 组件是否已创建

  • userIp变量是否初始化为空字符串

  • userTodos变量是否初始化为空数组

  • 当本地通知被触发时(这是我们对 Ionic Native 插件进行单元测试的方式)

使用以下代码更新todoapp_v2/src/pages/home/home.spec.ts

import { async, TestBed } from '@angular/core/testing'; 
import { IonicModule, NavController, AlertController } from 'ionic-angular'; 
import { MyApp } from '../../app/app.component'; 
import { HomePage } from './home'; 
import { LoginPage } from '../login/login'; 
import { IonicStorageModule } from '@ionic/storage'; 
import { LocalNotifications } from '@ionic-native/local-notifications'; 
import { LocalNotificationsMocks } from '../../mocks/localNotificationMocks'; 
import { Auth } from '../../providers/auth'; 
import { IP } from '../../providers/ip'; 
import { Todos } from '../../providers/todos'; 

describe('Component: Home Component', () => { 
  let fixture; 
  let component; 
  let localNotif; 

  beforeEach(async(() => { 
    TestBed.configureTestingModule({ 
      declarations: [ 
        MyApp, 
        HomePage, 
        LoginPage 
      ], 
      imports: [ 
        IonicModule.forRoot(MyApp), 
        IonicStorageModule.forRoot() 
      ], 
      providers: [ 
        Auth, 
        Todos, 
        IP, 
        { provide: LocalNotifications, useClass: 
          LocalNotificationsMocks }, 
        NavController, 
        AlertController 
      ] 
    }) 
  })); 

  beforeEach(() => { 
    fixture = TestBed.createComponent(HomePage); 
    component = fixture.componentInstance; 
    localNotif = new LocalNotificationsMocks(); 
  }); 

  it('should be created', () => { 
    expect(component instanceof HomePage).toBe(true); 
  }); 

  it('should initialize `userIp` to ''', () => { 
    expect(component.userIp).toBe(''); 
  }); 

  it('should initialize `userTodos`', () => { 
    expect(component.userTodos.length).toBe(0); 
  }); 

  // this is how we mock and test 
  // ionic-native plugins 
  it('should return null when a new notification is scheduled', () => { 
    expect(component.notify()).toBe(localNotif.schedule()); 
  }); 
});

从上述代码中需要注意的关键事项是提供者的属性传递给TestBed.configureTestingModule()。由于我们在模拟环境中运行测试,其中没有 Cordova,我们需要模拟或模拟LocalNotifications服务。

我们这样做的方式是创建另一个名为LocalNotificationsMocks的类,并在调用LocalNotifications时使用它。在LocalNotificationsMocks中,我们实现了返回预定义值的虚拟方法来模拟服务。

因此,我们将为LocalNotifications创建一个模拟服务。在src文件夹内创建一个名为 mocks 的文件夹。在mocks文件夹内,创建一个名为localNotificationMocks.ts的文件。使用以下代码更新todoapp_v2/src/mocks/localNotificationMocks.ts

export class LocalNotificationsMocks { 
  public schedule(config: any): void { 
    // https://github.com/driftyco/ionic-
    native/blob/5aa484c024d7cac3b6628c5dd8694395e8a29ed4/src/%40ionic-
    native/plugins/local-notifications/index.ts#L160 
    return; 
  } 
}

我们正在覆盖schedule()以根据原始定义返回 void。

完成组件测试后,接下来我们将测试提供者。

todoapp_v2/src/providers文件夹内创建一个名为ip.spec.ts的文件。在这个提供者中,我们将模拟一个 HTTP 请求,并将模拟响应的输出与硬编码的响应进行比较。我们将测试以下情况:

  • 提供者是否被构建

  • 从模拟后端服务获取 IP 地址

打开todoapp_v2/src/providers/ip.spec.ts并使用以下代码进行更新:

import { async, TestBed, inject } from '@angular/core/testing'; 
import { IP } from './ip'; 
import { Headers, Http, HttpModule, BaseRequestOptions, XHRBackend, Response, ResponseOptions } from '@angular/http'; 
import { MockBackend, MockConnection } from '@angular/http/testing'; 

// https://kendaleiv.com/angular-2-mockbackend-service-testing-template-using-testbed/ 
describe('Service: IPService', () => { 
  let service; 
  let http; 

  const mockResponse = { 
    ip: '11:22:33:44' 
  }; 

  beforeEach(async(() => { 
    TestBed.configureTestingModule({ 
      imports: [ 
        HttpModule 
      ], 
      providers: [ 
        MockBackend, 
        BaseRequestOptions, 
        { 
          provide: Http, 
          useFactory: (backend, options) => new Http(backend, options), 
          deps: [MockBackend, BaseRequestOptions] 
        }, 
        IP 
      ] 
    }) 
  })); 

  it('should construct', async(inject( 
    [IP, MockBackend], (ipService, mockBackend) => { 
      expect(ipService).toBeDefined(); 
    }))); 

  it('should get IP equal to `11:22:33:44`', async(inject( 
    [IP, MockBackend], (ipService, mockBackend) => { 

      mockBackend.connections.subscribe(conn => { 
        conn.mockRespond(new Response(new ResponseOptions({ body: JSON.stringify(mockResponse) }))); 
      }); 

      const result = ipService.get(); 

      result.subscribe((res) => { 
        expect(res.json()).toEqual({ 
          ip: '11:22:33:44' 
        }); 
      }); 
    }))); 
});

请注意 HTTP 的提供者。我们已经将它连接到MockBackend,并在发出请求时返回一个mockResponse

接下来是 Auth 提供者。在todoapp_v2/src/providers文件夹内创建一个名为auth.spec.ts的文件。我们将在这个提供者中测试以下内容:

  • 提供者是否被构建

  • 成功使用有效凭据登录

  • 使用无效凭据成功失败

  • isAuthenticated()的值

  • logout()authStatus的值

打开todoapp_v2/src/providers/auth.spec.ts并使用以下代码进行更新:

import { async, TestBed, inject } from '@angular/core/testing'; 
import { Auth } from './auth'; 
import { IonicStorageModule } from '@ionic/storage'; 
import { StorageMocks } from '../mocks/storageMocks'; 

let validUser = { 
  email: 'a@a.com', 
  password: 'a' 
} 

let inValidUser = { 
  email: 'a@a.com', 
  password: 'b' 
} 

describe('Service: AuthService', () => { 
  beforeEach(async(() => { 
    TestBed.configureTestingModule({ 
      imports: [ 
        IonicStorageModule.forRoot() 
      ], 
      providers: [ 
        Auth, 
        { provide: IonicStorageModule, useClass: StorageMocks }, 
      ] 
    }); 

  })); 

  it('should construct', async(inject( 
    [Auth, IonicStorageModule], (authService, ionicStorageModule) => { 
      expect(authService).toBeDefined(); 
    }))); 

  it('should login user with valid credentials', async(inject( 
    [Auth, IonicStorageModule], (authService, ionicStorageModule) => { 
      expect(authService.login(validUser)).toBeTruthy(); 
    }))); 

  it('should not login user with invalid credentials', async(inject( 
    [Auth, IonicStorageModule], (authService, ionicStorageModule) => { 
      expect(authService.login(inValidUser)).toBeFalsy(); 
    }))); 

  it('should return the auth status as true', async(inject( 
    [Auth, IonicStorageModule], (authService, ionicStorageModule) => { 
      // log the user in! 
      authService.login(validUser); 
      let result = authService.isAuthenticated(); 

      result.then((status) => { 
        expect(status).toBeTruthy(); 
      }) 
    }))); 

  it('should set auth to falsy on logout', async(inject( 
    [Auth, IonicStorageModule], (authService, ionicStorageModule) => { 
      // log the user in! 
      let authStatus = authService.login(validUser); 
      // check if login is successful 
      expect(authStatus).toBeTruthy(); 

      // trigger logout 
      let result = authService.logout(); 
      result.then((status) => { 
        expect(status).toBeFalsy(); 
      }); 
    }))); 

});

为了成功执行上述测试用例,我们需要模拟IonicStorageModule。在todoapp_v2/src/mocks文件夹内创建一个名为storageMocks.ts的新文件。使用以下代码更新todoapp_v2/src/mocks/storageMocks.ts

export class StorageMocks { 
  // mock store   
  store = {}; 

  public get(key) { 
    return new Promise((resolve, reject) => { 
      resolve(this.store[key]); 
    }); 
  } 

  public set(key, value){ 
    return new Promise((resolve, reject) => { 
      this.store[key] = value; 
      resolve(this.store[key]); 
    }); 
  } 
}

在这里,我们正在使用内存对象覆盖IonicStorageModule的行为。

我们将要测试的最后一个提供者是 Todos。在todoapp_v2/src/providers文件夹内创建一个名为todos.spec.ts的文件。我们将测试以下内容:

  • 提供者是否被构建

  • Todos 的初始长度为0

  • 保存一个 todo

  • 更新一个 todo

  • 删除一个 todo

打开todoapp_v2/src/providers/todos.spec.ts并进行以下更新:

import { async, TestBed, inject } from '@angular/core/testing'; 
import { Todos } from './todos'; 
import { IonicStorageModule } from '@ionic/storage'; 
import { StorageMocks } from '../mocks/storageMocks'; 

let todos = [{ 
  text: 'Buy Eggs', 
  isCompleted: false 
}]; 

describe('Service: TodoService', () => { 
  beforeEach(async(() => { 
    TestBed.configureTestingModule({ 
      imports: [ 
        IonicStorageModule.forRoot() 
      ], 
      providers: [ 
        Todos, 
        { provide: IonicStorageModule, useClass: StorageMocks }, 
      ] 
    }); 

  })); 

  it('should construct', async(inject( 
    [Todos, IonicStorageModule], (todoService, ionicStorageModule) => { 
      expect(todoService).toBeDefined(); 
    }))); 

  it('should fetch 0 todos initally', async(inject( 
    [Todos, IonicStorageModule], (todoService, ionicStorageModule) => { 
      let result = todoService.get(); 
      result.then((todos) => { 
        expect(todos).toBeFalsy(); 
      }); 
    }))); 

  it('should save a todo', async(inject( 
    [Todos, IonicStorageModule], (todoService, ionicStorageModule) => { 
      let result = todoService.set(todos); 
      result.then((_todos) => { 
        expect(_todos).toEqual(todos); 
        expect(_todos.length).toEqual(1); 
      }); 
    }))); 

   it('should update a todo', async(inject( 
    [Todos, IonicStorageModule], (todoService, ionicStorageModule) => { 
      let todo = todos[0]; 
      todo.isCompleted = true; 
      todos[0] = todo; 
      let result = todoService.set(todos); 
      result.then((_todos) => { 
        expect(_todos[0].isCompleted).toBeTruthy(); 
      }); 
    })));  

   it('should delete a todo', async(inject( 
    [Todos, IonicStorageModule], (todoService, ionicStorageModule) => { 
      todos.splice(0, 1); 
      let result = todoService.set(todos); 
      result.then((_todos) => { 
        expect(_todos.length).toEqual(0); 
      }); 
    })));  

});

请注意提供者中的StorageMocks设置。通过这样做,我们已经完成了编写测试用例。下一步是执行。

执行单元测试

为了开始执行过程,我们将在package.json文件中添加一个脚本,这样我们就可以通过在命令提示符/终端中执行npm test来轻松运行测试。

打开package.json并在 scripts 部分添加以下行:

"test": "karma start --reporters html ./test-config/karma.conf.js"

现在运行以下命令:

npm test

然后,您应该看到浏览器启动并执行我们的测试用例。命令提示符/终端日志应该看起来像这样:

todoapp_v2 npm test

> ionic-hello-world@ test /chapter9/todoapp_v2
> karma start --reporters html ./test-config/karma.conf.js

webpack: Compiled successfully.
webpack: Compiling...
ts-loader: Using typescript@2.0.9 and 
    /chapter9/todoapp_v2/tsconfig.json

webpack: Compiled successfully.
26 03 2017 23:26:55.201:INFO [karma]: Karma v1.5.0 server started 
    at http://0.0.0.0:9876/
26 03 2017 23:26:55.204:INFO [launcher]: Launching browser Chrome 
    with unlimited concurrency
26 03 2017 23:26:55.263:INFO [launcher]: Starting browser Chrome
26 03 2017 23:26:57.491:INFO [Chrome 56.0.2924 (Mac OS X 10.12.1)]: 
    Connected on socket DHM_DNgQakmVtg7RAAAA with id 44904930

您还应该看到一个名为unit-test-report.html的文件创建在test-config文件夹内。如果在浏览器中打开此文件,您应该会看到以下内容:

上表总结了执行的测试。

driftyco/ionic-unit-testing-example

在撰写本章的三天前,Ionic 团队发布了一篇博客文章,表明他们将支持单元测试和端到端测试,并且这将成为 Ionic 脚手架项目本身的一部分。更多信息可以在这里找到:blog.ionic.io/basic-unit-testing-in-ionic/

这个项目是基于 Ionic 2 测试领域中的一些非常有价值的贡献者,正如博客文章中所提到的。截至今天,driftyco/ionic-unit-testing-examplegithub.com/driftyco/ionic-unit-testing-example)存储库没有完整的实现,只支持单元测试。

但到书出版时,他们可能已经推出了。driftyco/ionic-unit-testing-example内的设置应该仍然与我们在这里遵循的设置相同。我提醒您这一点,以便您可以关注该项目。

E2E 测试

在单元测试中,我们已经测试了代码单元。在端到端测试中,我们将测试完整的功能,比如登录或注销,或者获取 IP 地址等等。在这里,我们将整个应用程序作为一个整体来看,而不仅仅是一个功能的一部分。有些人也将这称为集成测试。

我们将使用 Protractor 来帮助我们执行 E2E 测试。我们仍然会使用 Jasmine 来描述我们的测试,只是测试运行器从 Karma 变为 Protractor。

引用自www.protractortest.org

"Protractor 是一个用于 Angular 应用程序的端到端测试框架。Protractor 在真实浏览器中运行测试,与用户交互。"

YouTube 上有很多视频,深入解释了 Protractor 和 Selenium,以及 Protractor 的各种 API,可以用于测试,如果您想了解更多关于 Protractor 的信息。

我们将要进行的测试如下:

  • 登录到应用程序

  • 验证登录

  • 注销应用程序

  • 验证注销

设置项目

我将按照名为“E2E(端到端)测试在 Ionic 2 中的介绍”(www.joshmorony.com/e2e-end-to-end-testing-in-ionic-2-an-introduction/)的文章来设置 E2E 环境。

我们将使用相同的示例来实现单元测试。

首先通过运行以下命令安装 protractor:

npm install protractor --save-dev

接下来,安装webdriver-manager并更新它:

npm install -g webdriver-manager
webdriver-manager update

现在,我们将通过运行以下命令安装 Protractor 的依赖项:

npm install jasmine-spec-reporter ts-node connect @types/jasmine@2.5.41 
@types/node --save-dev

请注意 Jasmine 类型的版本。它是硬编码为2.5.41。在撰写本文时,TypeScript 版本的 Jasmine 类型与 Ionic 2 项目存在一些冲突。如果您正在使用 Ionic 3.0,则应该已经解决了这个问题。

接下来,在todoapp_v2项目文件夹的根目录下,创建一个名为protractor.conf.js的文件。使用以下代码更新todoapp_v2/protractor.conf.js

var SpecReporter = require('jasmine-spec-reporter').SpecReporter; 

exports.config = { 
    allScriptsTimeout: 11000, 
    directConnect: true, 
    capabilities: { 
        'browserName': 'chrome' 
    }, 
    framework: 'jasmine', 
    jasmineNodeOpts: { 
        showColors: true, 
        defaultTimeoutInterval: 30000, 
        print: function() {} 
    }, 
    specs: ['./e2e/**/*.e2e-spec.ts'], 
    baseUrl: 'http://localhost:8100', 
    useAllAngular2AppRoots: true, 
    beforeLaunch: function() { 

        require('ts-node').register({ 
            project: 'e2e' 
        }); 

        require('connect')().use(require('serve-static')
        ('www')).listen(8100); 

    }, 
    onPrepare: function() { 
        jasmine.getEnv().addReporter(new SpecReporter()); 
    } 
}

这个文件定义了 Protractor 和 Selenium 的启动属性。

接下来,我们将在todoapp_v2文件夹的根目录下创建一个名为e2e的文件夹。在todoapp_v2/e2e文件夹内,创建一个名为tsconfig.json的文件。使用以下代码更新todoapp_v2/e2e/tsconfig.json

{ 
  "compilerOptions": { 
    "sourceMap": true, 
    "declaration": false, 
    "moduleResolution": "node", 
    "emitDecoratorMetadata": true, 
    "experimentalDecorators": true, 
    "lib": [ 
      "es2016" 
    ], 
    "outDir": "../dist/out-tsc-e2e", 
    "module": "commonjs", 
    "target": "es6", 
    "types":[ 
      "jasmine", 
      "node" 
    ] 
  } 
}

这完成了我们的端到端测试设置。现在,我们将开始编写测试。

编写 E2E 测试

现在我们已经完成了所需的设置,我们将开始编写测试。在todoapp_v2/e2e文件夹内创建一个名为test.e2e-spec.ts的新文件。

如前所述,我们将执行一个简单的测试--登录到应用程序,验证登录,从应用程序注销,并验证注销。所需的测试应该如下所示:

import { browser, element, by, ElementFinder } from 'protractor'; 

// https://www.joshmorony.com/e2e-end-to-end-testing-in-ionic-2-an-introduction/ 
describe('Check Navigation : ', () => { 

  beforeEach(() => { 
    browser.get(''); 
  }); 

  it('should have `Todo App (v2)` as the title text on the Login Page', 
  () => { 
      expect(element(by.css('.toolbar-title')) 
        .getAttribute('innerText')) 
        .toContain('Todo App (v2)'); 

  }); 

  it('should be able to login with prefilled credentials', () => { 
    element(by.css('.scroll-content > button')).click().then(() => { 
      // Wait for the page transition 
      browser.driver.sleep(3000); 

      // check if we have really redirected 
      expect(element(by.css('.scroll-content > button')) 
        .getAttribute('innerText')) 
        .toContain('ADD TODO'); 

      expect(element(by.css('h2.text-center')) 
        .getAttribute('innerText')) 
        .toContain('No Todos'); 

      expect(element(by.css('ion-footer > h3')) 
        .getAttribute('innerText')) 
        .toContain('Your IP : 183.82.232.178'); 

    }); 

  }); 

  it('should be able to logout', () => { 
     element(by.css('ion-buttons > button')).click().then(() => { 

      // Wait for the page transition 
      browser.driver.sleep(3000); 

      // check if we have really redirected 
      expect(element(by.css('.toolbar-title')) 
        .getAttribute('innerText')) 
        .toContain('Todo App (v2)'); 
    }); 
  }); 

});

前面的代码是不言自明的。请注意,我已经将我的 IP 地址硬编码以在测试时进行验证。在开始执行 E2E 测试之前,请更新 IP 地址。

执行 E2E 测试

现在我们已经完成了测试的编写,我们将执行相同的测试。在项目的根目录下打开命令提示符/终端,并运行以下命令:

protractor

您可能会遇到一个错误,看起来像这样:

// snipp
Error message: Could not find update-config.json. Run 'webdriver-
manager update' to download binaries.
// snipp

如果是这样,请运行以下命令:

./node_modules/protractor/bin/webdriver-manager update

然后运行protractor./node_modules/.bin/protractor

然后您应该会看到浏览器启动并导航到应用程序。如果一切顺利,您应该会在命令提示符/终端中看到以下输出:

![](https://gitee.com/OpenDocCN/freelearn-html-css-zh/raw/master/docs/lrn-ionic-2e/img/00118.jpeg)  todoapp_v2 ./node_modules/.bin/protractor
[00:37:27] I/launcher - Running 1 instances of WebDriver
[00:37:27] I/direct - Using ChromeDriver directly...
Spec started

 Check Navigation :
![](https://gitee.com/OpenDocCN/freelearn-html-css-zh/raw/master/docs/lrn-ionic-2e/img/00119.jpeg) should have `Todo App (v2)` as the title text on the Login Page
![](https://gitee.com/OpenDocCN/freelearn-html-css-zh/raw/master/docs/lrn-ionic-2e/img/00119.jpeg) should be able to login with prefilled credentials
![](https://gitee.com/OpenDocCN/freelearn-html-css-zh/raw/master/docs/lrn-ionic-2e/img/00119.jpeg) should be able to logout

Executed 3 of 3 specs SUCCESS in 11 secs.
[00:37:40] I/launcher - 0 instance(s) of WebDriver still running
[00:37:40] I/launcher - chrome #01 passed

通过这样,我们完成了对 Ionic 应用的两种主要测试。

我们要做的最后一个测试是使用 AWS 设备农场。

注意:在测试 Cordova 功能时,您可以像之前看到的那样模拟它们。我们将在执行 E2E 测试之前直接更新app.module.ts,而不是更新测试床。但是请记住在测试完成后将其改回来。

代码覆盖率

检查代码覆盖率是测试过程中非常重要的活动。代码覆盖率帮助我们了解我们编写的代码有多少被测试了。您可以参考karma-coverage (github.com/karma-runner/karma-coverage) 模块和 remap-istanbul (github.com/SitePen/remap-istanbul) 模块来实现代码覆盖率。

您还可以参考如何向 Angular 2 项目添加测试覆盖报告www.angularonrails.com/add-test-coverage-report-angular-2-project/ 进行进一步参考。

AWS 设备农场

现在我们已经对我们的应用进行了单元测试和端到端测试,我们将部署应用到实际设备上并进行测试。

要在实际设备上开始测试,我们需要借用或购买这些设备,这对于一个一次性的应用来说可能并不实际。这就是设备农场的概念出现的地方。设备农场是各种设备的集合,可以通过 Web 界面访问。这些设备可以通过 Web 进行访问和测试,方式类似于在实际设备上进行测试。

市面上有很多提供按需付费设备农场的供应商。在许多设备农场的试错之后,我对 AWS 设备农场有了一些好感。它简单易用,并且在错误日志、截图和视频方面非常详细。后者真的可以帮助您在特定设备上识别终端用户或错误崩溃报告中报告的问题。

截至撰写本章的日期,AWS 每个设备每分钟收费$0.17,前 250 分钟免费。或者如果您是重度用户,您也可以根据您的使用情况订阅无限测试计划。这从每月$250 起。

在这个主题中,使用 AWS 设备农场,我们将上传我们在第八章 Ionic 2 迁移指南中迁移的 Todo 应用的 APK,并执行两个测试:

  • Monkey 测试应用,看看应用是否崩溃

  • 在实际设备上手动测试应用

设置 AWS 设备农场

在我们开始在实际设备上测试之前,我们将设置一个新的 AWS 账户,如果您还没有的话。您可以转到aws.amazon.com/ 进行注册和登录。

一旦您进入 AWS 控制台,从页面头部的服务选项中选择设备农场。设备农场是 AWS 区域不可知的。您不需要在特定区域才能访问它。

一旦您进入 AWS 设备农场的主页,您应该会看到一个像这样的屏幕:

点击“开始”。这将提示我们输入项目名称。在 Device Farm 中,项目是我们要执行的测试类型、要测试的设备类型或应用程序版本的逻辑分组。

我将把我的项目命名为Todo App v1。当我有另一个版本时,我将把它命名为Todo App v2

注意:这里的v1指的是我们的 Todo 应用的 v1 版本,而不是使用 Ionic v1 构建的 Todo 应用。

点击“创建项目”,你应该会进入项目主页。

设置 Todo 应用

现在我们准备测试我们的应用,让我们继续构建它。转到todoapp_v2文件夹并打开一个新的命令提示符/终端。运行ionic platform add androidionic platform add ios,然后构建应用程序:

ionic build

在这个例子中,我将为 Android 构建并使用 APK 进行设备测试。构建完成后,转到todoapp_v2/platforms/android/build/outputs/apk文件夹,你应该会找到一个名为android-debug.apk的文件。我们将上传这个 APK 文件进行测试。

iOS 测试的流程也类似,只是我们上传 IPA 文件。

对 Todo 应用进行猴子测试

猴子测试或模糊测试是一种自动化测试技术,测试执行器将输入随机输入,在应用程序或页面的随机部分执行随机点击,以查看应用程序是否崩溃。要了解更多关于猴子测试的信息,请参考:en.wikipedia.org/wiki/Monkey_testing

Device Farm 将这作为在设备上测试应用程序的良好起点。

一旦我们进入项目主页,我们应该会看到两个选项卡:自动化测试和远程访问:

在自动化测试选项卡上,点击“创建新运行”。在“选择您的应用程序”部分,选择您的选择,如下面的截图所示:

接下来上传 APK 或 IPA 文件。一旦应用程序成功上传,我们应该会看到类似于这样的东西:

点击“下一步”。

在配置测试部分,选择内置:模糊,如下面的截图所示:

还有其他自动化测试框架,如 Appium 或 Calabash,也可以用来构建自动化测试套件。Device Farm 也支持这些框架。

点击“下一步”。

这是我们选择目标设备的地方。默认情况下,AWS Device Farm 选择顶级设备。我们可以选择这个,也可以构建自己的设备池:

在这个例子中,我将选择顶部设备。

点击“下一步”以进入指定设备状态部分。在这里,如果需要,我们可以覆盖设备功能:

我们将保持现状。

点击“下一步”,在这里我们设置测试的估计时间。我选择了每个设备 5 分钟,如下所示:

点击“确认并开始运行”以启动猴子测试。这将需要大约 25 分钟才能完成。你可以去跑步,喝咖啡,做瑜伽,基本上你需要度过 25 分钟。

现在测试已经完成,你应该会看到这样的屏幕:

看起来 Todo 应用在五台设备上通过了猴子测试。如果我们点击该行,我们应该会看到结果的深入分析:

正如你从前面的步骤中看到的那样,我们可以查看每个设备的结果和所有设备的截图。为了获得更深入的见解,我们将点击一个设备:

正如你从前面的图片中看到的那样,我们还可以查看测试执行视频、日志、性能和截图:

性能概述如前面的截图所示。

这有助于我们快速在各种设备上对我们的应用进行一些随机测试。

在各种设备上手动测试 Todo 应用

在本节中,我们将远程访问设备并在其上测试我们的应用程序。当用户报告您无法在其他设备上复制的特定设备上的错误时,此功能非常有用。

要开始手动测试,请导航到项目主页,然后单击“远程访问”选项卡。然后单击“开始新会话”按钮。

这将重定向到另一个页面,在那里我们需要选择一个设备,如图所示:

我选择了一个 Android 设备,并通过单击“确认并开始会话”来启动了一个新会话。这将启动一个新会话:

一旦设备可用,我们应该看到类似于这样的东西:

默认情况下,我们最近上传的 APK 将安装在此设备上。否则,您可以使用右上角的“安装应用程序”来安装特定应用程序,如前面的屏幕截图所示。

我已经从菜单中导航到TodoApp-v2,如图所示:

启动应用程序后,我们可以进行登录、管理待办事项、查看通知等操作:

测试完成后,我们可以停止会话。会话成功终止后,我们可以以可下载的格式获取日志、视频和网络流量的副本以进行进一步调试:

通过这种方式,我们已经看到了如何在各种设备上手动测试应用程序。

自动化测试

除了上述测试应用的方法之外,我们还可以使用诸如 Appium(appium.io/)之类的框架构建自动化测试用例。通过使用设备农场,我们可以上传 APK 或 IPA,然后进行自动化测试套件。然后我们选择一组设备并在它们上执行测试。

您可以查阅自动化混合应用appium.io/slate/en/master/?ruby#automating-hybrid-apps)和使用 Smoke Tests 和 Appium 验证 Cordova 或 PhoneGap 构建ezosaleh.com/verifying-a-cordovaphonegap-build-with-smoke-tests-appium)来了解为混合应用编写自动化测试的想法。

如果选择,您也可以在模拟器中本地运行这些自动化测试。

总结

在本章中,我们已经介绍了测试的两种主要方法-单元测试和端到端测试。我们使用 Karma 和 Jasmine 对 Todo 应用进行了单元测试。我们使用 Protractor 和 Jasmine 进行了端到端测试。我们还使用了 AWS 设备农场的模糊测试来测试我们的应用,以及通过在我们选择的远程设备上安装应用程序来进行测试。

在下一章中,我们将看一下发布和管理 Ionic 应用程序。

第十章:发布 Ionic 应用

在本章中,我们将介绍三种为 Ionic 应用生成安装程序的方法。一种是使用 PhoneGap 构建服务,第二种是使用 Cordova CLI,最后一种是使用 Ionic 包服务。我们将为 Android 和 iOS 操作系统生成安装程序。本章将涵盖以下主题:

  • 生成图标和启动屏幕

  • 验证 config.xml

  • 使用 PhoneGap 构建服务生成安装程序

  • 使用 Cordova CLI 生成安装程序

  • 使用 Ionic 包生成服务

为应用程序准备分发

现在我们已经成功构建了 Ionic 应用,我们希望进行分发。通过应用商店是触及更广泛受众的最佳方式。但是,在开始分发应用之前,我们将需要特定于应用的图标和启动屏幕。启动屏幕是完全可选的,取决于产品理念。

设置图标和启动屏幕

默认情况下,当您运行以下代码时:

ionic platform add android 

或者

ionic platform add ios

CLI 会自动添加一个名为资源的新文件夹。您可以在第七章中查看这一点,构建 Riderr 应用。资源文件夹包括 Ionic 或 Android 或两者的子文件夹,具体取决于您添加了多少个平台,在每个文件夹中,您将看到两个名为图标和启动的子文件夹。

如果您的应用程序使用启动屏幕,则可以保留启动文件夹,否则删除该文件夹以节省最终应用程序安装程序的几个字节。

要生成图标,您可以获取大于 1024 x 1024 的图标副本,并使用任何服务,例如以下服务,为 Android 和 iOS 生成图标和启动屏幕:

我与上述任何服务都没有关联。您使用这些服务需自担风险。

或者,更好的是,您可以将名为icon.pngsplash.png的文件放在资源文件夹中,并运行以下代码:

ionic resources 

这将负责将您的图像上传到 Ionic 云,根据需要调整其大小,并将其保存回资源文件夹。

请注意,您正在将内容上传到公共/ Ionic 云中。

如果您只想转换图标,可以使用以下方法:

ionic resources --icon

如果只需要启动屏幕,可以使用以下方法:

ionic resources --splash

您可以使用code.ionicframework.com/resources/icon.psd来设计您的图标,使用code.ionicframework.com/resources/splash.psd来设计您的启动屏幕。

您可以将icon.png图像,icon.psd文件或 icon.ai 文件放在资源文件夹的根目录,ionic 资源将会自动处理!

更新 config.xml

一旦以上点都经过验证,我们将开始安装程序生成过程。

PhoneGap 构建服务

我们将首先看一下使用 PhoneGap 构建服务生成应用程序安装程序的方法。这可能是为 Android 和 iOS 生成安装程序的最简单方法。

这个过程非常简单。我们将整个项目上传到 PhoneGap 构建服务,它会负责构建安装程序。

如果你认为上传完整项目不切实际,你可以只上传www文件夹。但是,你需要做以下更改。

  1. config.xml移动到www文件夹内。

  2. 将资源文件夹移动到www文件夹内。

  3. config.xml中更新资源文件夹的路径。

如果你经常做以上操作,我建议使用一个构建脚本来生成一个带有以上更改的 PhoneGap 构建Deployable文件夹。

如果你计划只为 Android 发布你的应用程序,你不需要做任何其他事情。但是,如果你计划生成 iOS 安装程序,你需要获得一个苹果开发者账户,并按照docs.build.phonegap.com/en_US/signing_signing-ios.md.html中的步骤生成所需的证书。

你也可以按照docs.build.phonegap.com/en_US/signing_signing-android.md.html中提到的步骤签署你的 Android 应用程序。

一旦你拥有所需的证书和密钥,我们就可以开始生成安装程序了。你可以按照以下步骤使过程变得简单:

  1. 创建一个 PhoneGap 账户并登录(build.phonegap.com/plans

  2. 接下来,转到build.phonegap.com/people/edit,选择 Signing Keys 选项卡,并上传 iOS 和 Android 证书。

  3. 接下来,转到:build.phonegap.com/apps,点击 New App。作为免费计划的一部分,只要从公共 Git 存储库中拉取,你可以拥有尽可能多的应用。或者,你可以从私有存储库创建私有应用,或者通过上传 ZIP 文件创建。

  4. 为了测试服务,你可以创建一个.zip文件(不是.rar.7z),具有以下文件夹结构:

  • App(根文件夹)

  • config.xml

  • resources(文件夹)

  • www(文件夹)

这就是 PhoneGap 构建工作所需的一切。

  1. 将 ZIP 文件上传到build.phonegap.com/apps并创建应用程序。

这个过程通常需要大约一分钟来完成它的魔力。

有时,你可能会从构建服务中看到意外的错误。等一会儿,然后再试一次。根据流量的不同,有时构建过程可能会比预期的时间长一些。

使用 Cordova CLI 生成安装程序

我们将看一下使用 Cordova CLI 创建安装程序。

Android 安装程序

首先,我们将看一下使用 CLI 为 Android 生成安装程序。你可以按照以下步骤进行:

  1. 在项目的根目录打开一个新的命令提示符/终端。

  2. 使用以下命令移除不需要的插件:

 ionic plugin rm cordova-plugin-console

  1. 使用以下命令在发布模式下构建应用程序:
      cordova build --release android

这将在发布模式下生成一个未签名的安装程序,并将其放置在<<ionic project>>/platforms/android/build/outputs/apk/android-release-unsigned.apk

  1. 接下来,我们需要创建一个签名密钥。如果你已经有一个签名密钥,或者你正在更新一个现有的应用程序,你可以跳过下一步。

  2. 私钥是使用 keytool 生成的。我们将创建一个名为 deploy-keys 的文件夹,并将所有这些密钥保存在那里。创建文件夹后,运行cd命令进入文件夹并运行以下命令:

      keytool -genkey -v -keystore app-name-release-key.keystore -alias 
      alias_name -keyalg RSA -keysize 2048 -validity 10000 

您将被问到以下问题,您可以按照所示回答:

如果您丢失了此文件,您将永远无法提交更新到应用商店。

注意:要了解有关 keytool 和签名过程的更多信息,请参阅developer.android.com/studio/publish/app-signing.html

  1. 这是一个可选步骤,您也可以将android-release-unsigned.apk复制到deploy-keys文件夹中,并从那里运行以下命令。我会把文件留在原地。

  2. 接下来,我们使用 jarsigner 工具对未签名的 APK 进行签名:

      jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore app-name-
      release-key.keystore ../platforms/android/build/outputs/apk/android-
      release-unsigned.apk my-ionic-app

将要求输入密码,这是您在创建密钥库时的第一步输入的密码。签名过程完成后,现有的android-release-unsigned.apk将被替换为同名的已签名版本。

我们正在从 deploy-keys 文件夹内运行上述命令。

  1. 最后,我们运行zipalign工具来优化 APK:
      zipalign -v 4 ../platforms/android/build/outputs/apk/android-release-
      unsigned.apk my-ionic-app.apk

上述命令将在deploy-keys文件夹中创建my-ionic-app.apk

现在,您可以将此 APK 提交到应用商店。

iOS 安装程序

接下来,我们将使用 XCode 为 iOS 生成安装程序。您可以按照给定的步骤进行:

  1. 在项目的根目录打开新的命令提示符/终端。

  2. 删除不需要的插件:

 ionic plugin rm cordova-plugin-console

  1. 运行:
 ionic build -release ios

  1. 导航到 platforms/iOS 并使用 XCode 启动projectname.xcodeproj

  2. 一旦项目在 XCode 中,选择产品,然后从导航菜单中选择存档。

  3. 接下来,选择窗口并从导航菜单中选择组织者。您将看到一个创建的存档列表。

  4. 点击我们现在创建的快照存档,然后点击提交。进行帐户验证,然后应用将被上传到 iStore。

  5. 最后,您需要登录 iTunes 商店设置截图、描述等。

这结束了使用 Cordova CLI 生成安装程序的过程。

离子包

在本节中,我们将看一下 Ionic Package。

上传项目到 Ionic 云

使用 Ionic 云服务生成安装程序非常简单。首先,我们通过运行以下命令将我们的应用上传到我们的 Ionic 帐户:

ionic upload

在执行上述命令之前,请登录您的 Ionic 帐户。

如果您的项目涉及敏感信息,请在将应用上传到云之前与 Ionic 许可证进行交叉检查。

上传应用后,将为您的应用生成一个应用 ID。您可以在项目根目录下的ionic.config.json文件中找到应用 ID。

生成所需的密钥

您需要按照“使用 Cordova CLI 生成安装程序”部分的第 5 步,Android 安装程序子部分,获取密钥库文件。

接下来,我们使用 ionic package 命令生成安装程序:

ionic package <command> [options]

选项将包括以下内容:

例如,如果您想要以发布模式为 Android 生成安装程序,将如下所示:

ionic package release android -k app-name-release-key.keystore -a my-ionic-app -w 12345678 -r 12345678 -o ./ -e arvind.ravulavaru@gmail.com -p 12345678

我们正在从 deploy-keys 文件夹内运行上述命令。

同样,iOS 的上述命令将如下所示:

ionic package release ios -c certificate-file -d password -f profilefile -o ./ -e arvind.ravulavaru@gmail.com -p 12345678

摘要

在本章中,我们看到了如何发布和管理 Ionic 应用。我们看到了如何使用 PhoneGap 构建服务、使用 Cordova CLI 以及最后使用 Ionic Package 生成安装程序。

在下一章中,我们将看一下 Ionic 3 和 Ionic 2 与 Ionic 3 之间的主要区别。

请注意,到目前为止我们学到的几乎所有概念在 Ionic 3 中仍然适用。

第十一章:Ionic 3

在《学习 Ionic,第二版》的最后一章中,我们将看一下 Ionic 框架的最新变化--Ionic 3。我们还将简要介绍 Angular 及其发布。在本章中,我们将讨论以下主题:

  • Angular 4

  • Ionic 3

  • Ionic 3 的更新

  • Ionic 2 与 Ionic 3

Angular 4

自 Angular 2 发布以来,Angular 团队一直致力于使 Angular 成为一个稳定可靠的应用程序框架。2017 年 3 月 23 日,Angular 团队发布了 Angular 4。

什么?Angular 4?Angular 3 怎么了!!

简而言之,Angular 团队采用了语义化版本控制(semver.org/)来管理框架内所有包和依赖关系。在这个过程中,其中一个包(@angular/router)已经完全升级了一个主要版本,类似于以下情况,由于路由包的更改。:

框架 版本
@angular/core v2.3.0
@angular/compiler v2.3.0
@angular/compiler-cli v2.3.0
@angular/http v2.3.0
@angular/router V3.3.0

由于这种不一致性和为了避免未来的混淆,Angular 团队选择了 Angular 4 而不是 Angular 3。

此外,未来 Angular 版本的暂定发布时间表如下所示:

版本 发布日期
Angular 5 2017 年 9 月/10 月
Angular 6 2018 年 3 月
Angular 7 2018 年 9 月/10 月

您可以在angularjs.blogspot.in/2016/12/ok-let-me-explain-its-going-to-be.html上了解更多信息。

随着 Angular 4 的发布,一些重大的底层变化已经发生。以下是 Angular 4 的更新:

  • 更小更快,生成的代码更小

  • Animation包的更新

  • *ngIf*ngFor的更新

  • 升级到最新的 TypeScript 版本

要了解更多关于此版本的信息,请参阅angularjs.blogspot.in/2017/03/angular-400-now-available.html

由于 Ionic 遵循 Angular,他们已经将 Ionic 框架从版本 2 升级到版本 3,以将其基本 Angular 版本从 2 升级到 4。

Ionic 3

随着 Angular 4 的发布,Ionic 已经升级并转移到了 Ionic 3。

Ionic 版本 3(blog.ionic.io/ionic-3-0-has-arrived/)增加了一些新功能,如 IonicPage 和 LazyLoading。他们还将基本版本的 Angular 更新到了版本 4,并发布了一些关键的错误修复。有关更多信息,请参阅 3.0.0 的变更日志:github.com/driftyco/ionic/compare/v2.3.0...v3.0.0

Ionic 2 到 Ionic 3 的变化并不像我们从 Ionic 1 到 Ionic 2 看到的那样是破坏性的。Ionic 3 的变化更多地是增强和错误修复,这是在 Ionic 2 的基础上进行的。

Ionic 3 的更新

现在,我们将看一下 Ionic 3 的一些关键更新。

TypeScript 更新

对于 Ionic 3 的发布,Ionic 团队已经将 TypeScript 的版本更新到了最新版本。最新版本的 TypeScript 在构建时间和类型检查等方面有所增强。有关 TypeScript 更新的完整列表,请参阅 TypeScript 2.2 发布说明:www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html

Ionic 页面装饰器

Ionic 页面装饰器有助于更好地实现深度链接。如果你还记得我们在第四章中的导航示例,Ionic 装饰器和服务,我们在使用 Nav Controller 推送和弹出页面时引用了实际的类名。

我在这里指的是example9/src/pages/home/home.ts

import { Component } from '@angular/core'; 
import { NavController } from 'ionic-angular'; 
import { AboutPage } from '../about/about'; 

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

  constructor(public navCtrl: NavController) {} 

  openAbout(){ 
   this.navCtrl.push(AboutPage); 
  } 
} 

我们可以使用@IonicPage装饰器来实现相同的功能,如图所示。

让我们更新example9/src/pages/about/about.ts,如图所示:

import { Component } from '@angular/core'; 
import { NavController, IonicPage } from 'ionic-angular'; 

@IonicPage({ 
   name : 'about' 
}) 
@Component({ 
  selector: 'page-about', 
  templateUrl: 'about.html' 
}) 
export class AboutPage { 

  constructor(public navCtrl: NavController) {} 

  goBack(){ 
   this.navCtrl.pop(); 
  } 
} 

请注意,@IonicPage装饰器已经添加到@Component装饰器中。现在,我们将更新example9/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) {} 

  openAbout(){ 
   this.navCtrl.push('about'); 
  } 
} 

请注意this.navCtrl.push()的更改。现在,我们不再传递类的引用,而是传递了我们在example9/src/pages/about/about.ts中的@IonicPage装饰器属性的名称。此外,现在页面将在 URL 中添加名称,即localhost:8100/#/about

要了解更多关于 Ionic 页面装饰器的信息,请访问ionicframework.com/docs/api/navigation/IonicPage

还要查看 IonicPage 模块ionicframework.com/docs/api/IonicPageModule/,将多个页面/组件捆绑到一个子模块中,并在app.module.ts@NgModule中引用相同的内容。

懒加载

懒加载是 Ionic 3 发布的另一个新功能。懒加载让我们在需要时才加载页面。这将改善应用程序的启动时间并提高整体体验。

您可以通过访问docs.google.com/document/d/1vGokwMXPQItZmTHZQbTO4qwj_SQymFhRS_nJmiH0K3w/edit来查看在 Ionic 应用程序中实现懒加载的过程。

在撰写本章时,Ionic 3 已经发布了大约一周。CLI 和脚手架应用程序中存在一些问题/不一致。希望这些问题在书籍发布时能够得到解决。

Ionic 2 与 Ionic 3

在本书中,所有示例都是以 Ionic 2 为目标编写的。话虽如此,如果您使用 Ionic 3 开发您的 Ionic 应用程序,代码应该不会有太大变化。在所有脚手架应用程序中,您将注意到一个关键的区别是引入了 IonicPage 装饰器和 IonicPage 模块。

您可以随时参考 Ionic 文档,以获取有关这些 API 的最新版本的更多信息。

总结

通过这一点,我们结束了我们的 Ionic 之旅。

简而言之,我们从理解为什么选择 Angular、为什么选择 Ionic 和为什么选择 Cordova 开始。然后,我们看到移动混合应用程序的工作原理以及 Cordova 和 Ionic 的适用性。接下来,我们看了 Ionic 的各种模板,并了解了 Ionic 组件、装饰器和服务。之后,我们看了 Ionic 应用程序的主题设置。

接下来,我们学习了 Ionic Native,并了解了如何使用它。利用这些知识,我们构建了一个 Riderr 应用程序,该应用程序实现了 REST API,使用 Ionic Native 与设备功能进行交互,并让您感受到可以使用 Ionic 构建的完整应用程序的感觉。

之后,我们看了迁移 Ionic 1 应用程序到 Ionic 2 以及如何测试 Ionic 2 应用程序。在第十章,发布 Ionic 应用程序,我们看到了如何发布和管理我们的应用程序。

在本章中,我们看到了 Ionic 3 的关键变化。

查看附录获取更多有用信息和一些可以在生产应用程序中进行测试/使用的 Ionic 服务。

附录

本书的主要目的是让读者尽可能熟悉 Ionic。因此,我从第一章到第十一章采用了渐进式的方法,从 Cordova 的基础知识到使用 Angular Ionic 和 Cordova 构建应用程序。我们非常专注于学习 Ionic 的最低要求。

在本附录中,我将展示一些您可以探索的 Ionic CLI 和 Ionic Cloud 的更多选项。

Ionic CLI

Ionic CLI 每天都在变得更加强大。由于我们在整本书中一直在使用 Ionic CLI 2.1.14,我将讨论相同的选项。Ionic CLI 2.2.2 或更高版本也应该几乎具有相同的选项。

Ionic login

您可以通过以下三种方式之一登录到 Ionic Cloud 帐户。

首先,使用提示:

ionic login

其次,无提示:

ionic login --email arvind.ravulavaru@gmail.com --password 12345678

最后,使用环境变量。您可以将IONIC_EMAILIONIC_PASSWORD设置为环境变量,Ionic CLI 将在不提示的情况下使用它们。这可能有点不安全,因为密码将以纯文本形式存储。

注意:您需要拥有 Ionic Cloud 帐户才能成功进行身份验证。

Ionic start

首先,我们将看一下无 Cordova 标志选项。

无 Cordova

start 命令是创建新的 Ionic 应用程序的最简单方式之一。在本书中,我们一直使用 start 命令来始终创建一个新的 Cordova 和 Ionic 项目。

此外,Ionic 也可以在没有 Cordova 的情况下使用。

要在没有 Cordova 的情况下创建一个 Ionic 项目,您需要使用-w标志或--no-cordova标志运行 start 命令:

ionic start -a "My Mobile Web App" -i app.web.mymobile -w myMobileWebApp sidemenu

生成的项目应该如下所示:

. 
├── bower.json 
├── gulpfile.js 
├── ionic.config.json 
├── package.json 
├── scss 
│   ├── ionic.app.scss 
├── www 
    ├── css 
    ├── img 
    ├── index.html 
    ├── js 
    ├── lib 
    ├── manifest.json 
    ├── service-worker.js 
    ├── templates

现在,像往常一样,您可以cd进入myMobileWebApp文件夹并运行ionic serve

初始化支持 SCSS 的项目

初始化一个默认启用 SCSS 的项目,可以使用-s--sass标志运行 start 命令:

ionic start -a "My Sassy App" -i app.my.sassy --sass mySassyApp blank

注意:此命令在编写代码的当天不起作用。

列出所有 Ionic 模板

要查看所有可用模板的列表,请使用-l--list标志运行 Ionic start:

ionic start -l

截至今天,这些是可用的模板:

    blank ................ A blank starter project for Ionic
complex-list ......... A complex list starter template
maps ................. An Ionic starter project using Google Maps 
    and a side menu
salesforce ........... A starter project for Ionic and Salesforce
sidemenu ............. A starting project for Ionic using a side 
    menu with navigation in the content area
tabs ................. A starting project for Ionic using a simple 
    tabbed interface
tests ................ A test of different kinds of page navigation 

应用 ID

如果您使用 Ionic Cloud 服务,您将为在云上创建的每个项目分配一个应用 ID(有关更多信息,请参阅本章中的 Ionic Cloud 部分)。此应用 ID 将驻留在项目根目录下的ionic.config.json文件中。

当您创建一个新项目时,应用 ID 为空。如果您想将当前创建的项目与云上现有的应用关联起来,可以使用--io-app-id标志运行 start 命令,并将其传递给云生成的应用 ID:

ionic start -a "My IonicIO App" -i app.io.ionic --io-app-id "b82348b5" myIonicIOApp blank

现在,ionic.config.json应该如下所示:

    {
 "name": "My IonicIO App",
 "app_id": "b82348b5"
}

Ionic link

可以随时通过运行以下命令将本地创建的项目链接到云项目(有关更多信息,请参阅本章中的 Ionic Cloud 应用程序部分):

ionic link b82348b5

或者,您可以通过运行以下命令删除现有的应用 ID:

ionic link --reset

Ionic info

要查看已安装的库及其版本,请运行此命令:

ionic info

信息应该如下所示:

Cordova CLI: 6.4.0  
Ionic CLI Version: 2.1.14 
Ionic App Lib Version: 2.1.7 
ios-deploy version: 1.8.4  
ios-sim version: 5.0.6  
OS: macOS Sierra 
Node Version: v6.10.1 
Xcode version: Xcode 8.2.1 Build version 8C1002

Ionic state

使用 Ionic state 命令,您可以管理 Ionic 项目的状态。假设您正在为 Ionic 应用程序测试一些插件和平台。但是,如果它们失败,您不想使用它们。在这种情况下,您将使用保存和恢复命令。

您可以通过使用--nosave标志将插件或平台避免保存到package.json文件中:

ionic plugin add cordova-plugin-console --nosave

现在,您已经使用--nosave标志测试了您的应用程序,并且一切似乎都很正常。现在,您想将它们添加到您的package.json,您可以运行:

ionic state save

此命令查找您安装的插件和平台,然后将所需的信息添加到package.json文件中。您还可以选择仅通过分别使用--plugins--platforms标志运行前述命令来保存插件或平台。

一旦您添加了一堆插件,事情并不如预期那样工作,您可以通过运行以下命令重置到先前的状态:

ionic state reset

如果您想将应用程序恢复到 Cordova 插件和平台列表中,您可以在package.json中更新相同并运行:

ionic state restore

注意:reset命令会删除platformsplugins文件夹并重新安装它们,而restore只会在platformsplugins文件夹中恢复丢失的平台和插件。

Ionic 资源

当您添加新平台时,默认情况下会创建resources文件夹,并为给定平台创建图标和启动画面。这些图标和启动画面是默认图像。如果您想要为项目使用您的标志或图标,您只需要运行 Ionic 资源命令。

此命令将在resources文件夹中查找名为icon.png的图像,以为该操作系统的所有设备创建图标,并在resources文件夹中查找名为splash.png的图像,以为该操作系统的所有设备创建启动画面。

您可以用您的品牌图像替换这两个图像并运行:

    ionic resources

如果您只想转换图标,可以传入-i标志,如果只想转换启动画面,则可以传入-s标志。

注意:您还可以使用.png.psd(示例模板:code.ionicframework.com/resources/icon.psdcode.ionicframework.com/resources/splash.psd)或.ai文件来生成图标。您可以在此处找到更多信息:blog.ionic.io/automating-icons-and-splash-screens/

Ionic 服务器,模拟和运行

Ionic 提供了一种在浏览器、模拟器和设备中运行 Ionic 应用程序的简便方法。这三个命令中的每一个都带有一堆有用的选项。

如果您希望在调试时在模拟器和实际设备上运行实时重新加载,则可以使用-l标志进行实时重新加载,并使用-c启用在提示中打印 JavaScript 控制台错误。这绝对是 Ionic CLI 中最好且最常用的实用程序。此命令可以节省至少 30%的调试时间:

ionic serve -l -c
ionic emulate -l -c
ionic run -l -c

在使用 Ionic serve 时,您可以使用以下标志:

如果您的应用程序在 Android 和 iOS 上具有不同的外观和感觉,您可以通过运行同时测试这两个应用程序:

ionic serve --lab

您可以根据需要浏览先前列出的其他选项。

在使用 Ionic run 和 emulate 时,您可以使用以下选项:

这是相当不言自明的。

Ionic 上传和共享

您可以通过运行将当前的 Ionic 项目上传到您的 Ionic Cloud 帐户:

ionic upload

注意:您需要拥有 Ionic Cloud 帐户才能使用此功能。

一旦应用程序上传完成,您可以前往apps.ionic.io/apps查看新更新的应用程序。您可以使用共享命令与任何人分享此应用程序,并传递预期人员的电子邮件地址:

ionic share arvind.ravulavaru@gmail.com

Ionic 帮助和文档

随时可以通过运行查看所有 Ionic CLI 命令的列表:

ionic -h

您可以通过运行来打开文档页面:

ionic docs

要查看可用文档列表,您可以运行:

ionic docs ls

打开特定文档,您可以运行:

ionic docs ionicBody

Ionic Creator

如此惊人的 Ionic Creator 尚未适用于 Ionic 2。更多信息请参见:docs.usecreator.com/docs/ionic-2-support-roadmap

Ionic Cloud

您可以在apps.ionic.io/apps上创建和管理您的 Ionic 应用程序。在前述命令中,我们所指的应用程序 ID 是在使用apps.ionic.io/apps界面创建新应用程序时生成的应用程序 ID。

您可以通过单击apps.ionic.io/apps页面内的“新应用”按钮来创建新应用程序。创建应用程序后,您可以单击应用程序名称,然后将转到应用程序详细信息页面。

您可以通过单击应用程序详细信息页面上的“设置”链接来更新应用程序设置。

注意:您可以在这里阅读有关设置 Ionic 应用程序的更多信息:docs.ionic.io/

Ionic 云还提供其他服务,如 Auth、IonicDB、Deploy、Push 和 Package。

要使用这些服务中的任何一个,我们需要首先搭建一个 Ionic 应用程序,然后通过运行以下命令将此应用程序添加到 Ionic 云中:

ionic io init

接下来,您可以安装云客户端以与应用程序交互:

npm install @ionic/cloud-angular --save

完成后,我们在src/app/app.module.ts中设置云设置:

import { CloudSettings, CloudModule } from '@ionic/cloud-angular'; 

const cloudSettings: CloudSettings = { 
  'core': { 
    'app_id': 'APP_ID' 
  } 
}; 

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

现在我们已经准备好使用 Ionic 云服务了。

认证

使用 Auth 服务,我们可以轻松地对用户进行各种社交服务进行身份验证。我们不仅可以使用 Google、Twitter 和 LinkedIn 等社交服务,还可以设置简单的电子邮件和密码验证。您可以在这里查看身份验证提供程序的列表:docs.ionic.io/services/auth/#authentication-providers

使用Auth服务,这是我们管理身份验证的方式:

import { Auth, UserDetails, IDetailedError } from '@ionic/cloud-angular'; 

@Component({ 
   selector : 'auth-page' 
}) 
export class AuthPage { 
   private testUser: UserDetails = { 'email': 'user@domain.con', 'password': 'password' }; 

    // construct 
    constructor( 
        private auth: Auth, 
        private user: User) {} 

    signup() { 
        this.auth.signup(testUser).then(() => { 
            // testUser is now registered 
            console.log(this.user) 
            this.updateLastLogin(); // update user data 
        }, (err: IDetailedError < string[] > ) => { 
            for (let e of err.details) { 
                if (e === 'conflict_email') { 
                    alert('Email already exists.'); 
                } else { 
                    // handle other errors 
                } 
            } 
        }); 
    } 

    signin() { 
        this.auth.login('basic', testUser).then(() => { 
            // testUser is now loggedIn 
        }); 
    } 

    signout() { 
        this.auth.logout(); 
    } 

    updateLastLogin() { 
        if (this.auth.isAuthenticated()) { 
            this.user.set('lastLogin', new Date()); 
        } 
    } 
}

Auth service refer to: http://docs.ionic.io/services/auth/.

IonicDB

IonicDB 是一个无需担心可扩展性、数据管理和安全性的云托管实时数据库。如果您有使用 Firebase 或 Parse 的经验,IonicDB 与这些非常相似。

使用 IonicDB 的一个简单示例如下:

import {Database} from '@ionic/cloud-angular'; 

@Component({ 
    selector: 'todos-page' 
}) 
export class TodosPage { 
    public todos: Array < string > ; 

    constructor(private db: Database) { 
        db.connect(); 
        db.collection('todos').watch().subscribe((todos) => { 
            this.todos = todos; 
        }, (error) => { 
            console.error(error); 
        }); 
    } 

    createTodo (todoText: string) { 
        this.db.collection('todos').store({ text: todoText, isCompleted: false }); 
    } 
}

有关 IonicDB 的更多选项,请参阅docs.ionic.io/services/database/

部署

部署是另一个强大的服务,用户设备上安装的应用程序可以进行更新,而无需用户从应用商店更新。可以使用部署推送不涉及二进制更改的任何更改。

有关部署的更多信息,请参阅:docs.ionic.io/services/deploy

推送

推送服务允许应用程序所有者向其用户发送推送通知。推送服务还允许应用程序所有者根据类型对设备进行分段和定位,并允许仅向某些段发送通知。

推送通知使用 Phonegap Push 插件(github.com/phonegap/phonegap-plugin-push)与 FCM(Firebase Cloud Messaging)用于 Android 和 iOS 设备的 iOS 推送。

有关推送的更多信息,请参阅:docs.ionic.io/services/push/

打包

使用 Ionic 打包服务,开发人员可以为 Ionic 项目生成 APK 和 IPA,以与其他开发人员和测试人员共享。同样生成的 APK 和 IPA 也可以提交到 Play 商店和应用商店。

有关打包的更多信息,请参阅:docs.ionic.io/services/package/

摘要

在《学习 Ionic,第二版》的最后一章中,我们介绍了 Ionic CLI 的一些关键功能,并介绍了 Ionic 云服务。

希望这本书给您提供了一些关于开始使用 Ionic 2 的想法。

感谢您的阅读。

--阿文德·拉夫拉瓦鲁。

posted @ 2024-05-24 11:12  绝不原创的飞龙  阅读(7)  评论(0编辑  收藏  举报