第五课:类
上一个部分我们详细了解了什么是装饰器。概括起来就是类定义上面那一小段代码,声明这个类是什么、这个类需要什么、以及这个类应当如何配置。现在,我们要来聊一聊类了。
类是什么?
基于你自己的编程经历,你有可能知道,也有可能不知道类是什么。所以,这里我先后退一步,解释一下在同一编程理念里面类似什么,因为类不是Ionic,Angular或者JavaScript独有的概念。
类是面向对象编程(OOP)里面使用的,他们本质上是作为对象的‘蓝图’去使用的。你可以定义一个类,然后对他进行创建,实例化以及对象化。如果你对类一无所知,在继续进行课程之前请学习一些关于类的基本知识。那么,我们先来看一个简单的例子吧:
class Person {
constructor(name, age){
this.name = name;
this.age = age;
}
setAge(age){
this.age = age;
return true;
}
getAge(){
return this.age;
}
setName(name){
this.name = name;
return true;
}
getName(){
return this.name
}
isOld(){
return this.age > 50;
}
}
这个类定义了一个Person对象。constructor 构造器在创建类的实例时候会运行,他接受两个值:name和age。这两个值用于设置类的成员变量,也就是 this.name 和 this.age 。
这些值可以在类定义里面的任何地方通过this关键字来访问。this关键字是当前范围 scope的引用,所以他的引用取决于你在哪里使用他,但是如果你在一个类里面使用(不是回调里面或者其他将会改变范围的东西里面),那么他就是类本身了。
可以把你自己想象成this,你所在的物理世界是范围,考虑这个情况:如果你在旅馆的房间的话那么你的范围就是这个房间,如果你离开房间的话,那么你的范围就是旅馆里,如果你离开旅馆的话,那么你的范围就变成了世界(如果你想说国家的话也是可以的)。
如果你不熟悉这个关键字的话,推荐先阅读此文。
我们有自己的类定义,用于作为创建对象的蓝图,这样一来我们就可以像这样创建一个新的Person对象:
var john = new Person('John', 32);
这里我传入的两个参数将会传递到Person的constructor用于设置成员变量。如果我们现在运行如下代码:
console.log(john.getName());
John的名字将会输出到控制台。同理我们也可以调用getAge函数来得到他的年纪,我们也可以通过set函数来更改他的名字或者年龄。Getter和Setter在类里面非常常见,同时我们也定义了一个非常有趣的函数 isOld。这个函数在Person年龄大于50的时候返回true,咱们的Johnm看起来年纪是没有这么大的。
可能最需要记住的概念是类就是一个‘蓝图’,对象可以看到是类的一个独立副本。所以我们可以创建同一个类的多个实例,例如:
var john = new Person('John', 32);
var louise = new Person('Louise', 28);
var david = new Person('David', 52);
console.log(john.isOld());
console.log(louise.isOld());
console.log(david.isOld());
在以上代码中John,Louise和David都是Person类的独立个体对象,他们的值都是分开维护的。如果运行以上代码的话,只有David会返回(他可能比较老,但是可以肯定的是他很正直)true。
Ionic 2里面的类
现在,我们知道类是什么了,但是为什么他们会突然出现在Ionic 2和Angular 2里面呢?我们之前略表过,类是ES5规格的JavaScript的新特性。这个新特性当然是大受欢迎的,因为这个是编程界使用最广泛的模式,实际上JavaScript应用早就开始使用这种方式了,ES6只是把他正式化了。
ES6之前都是通过函数 functions来达到类型类的结构(估计现在大部分人还是这么做的,因为现在基本还是ES5的天下)。大概是这样的:
var Person = function (name, age) {
this.name = name;
this.age = age;
};
Person.prototype.isOld = function() {
return this.age > 50;
};
var david = new Person('david', 52);
console.log(david.isOld());
看起来稍微有一点点不同,但是最终结果还是基本一致的。由于ES6有了class关键字,我么现在可以用正‘正常’的途径来做了。
我们看一下Ionic 2里面类大概是怎样的:
import { Component } from '@angular/core';
import { NavController } from 'ionic-angular';
import { SomePage } from '../pages/some-page/some-page';
@Component({
selector: 'home-page',
templateUrl: 'home.html'
})
export class HomePage {
constructor(public nav: NavController) {
}
}
可能你第一眼就注意到了import语句了。你需要在类中用到的东西都需要导入。因此,我们从 @angular/core 导入 Component 这样我们就可以去使用 @Component 装饰器,以及 Ionic 库里的 NavController 用来控制页面导航。
同时我们也导入了我们自己创建的 SomePage。路径都是跟随项目目录结构的,在这个例子中 SomePage 组件定义在当前文件的上一层目录的 pages 文件夹里。导入路径是链接到 .ts 文件里的类,但是不需要加上 .ts 扩展名。
现在我们有了装饰器,在装饰器里面我们定义了他的选择器(即,这个组件在DOM中的名字,
定义完装饰器之后,我们终于达到了类本体了。我们注意到前面有一个 export 关键字,例如:
export class HomePage {
}
export 关键字和 import 关键字是串联的,我们想要在别的地方 import 类的话,我们得先 export。我们最后讨论的是构造器。我们已经大概的探讨了一下构造器在类中扮演的角色,这里也不例外:构造器里面的代码会在类实例化的时候运行。
此处我们不止需要知道这个。在Ionic 2中,类里面需要用到的服务都需要注入到构造器中,看起来将会是这样的:
constructor(platform: Platform, nav: NavController) {
platform.ready().then(() => {
});
}
例子中我们需要利用 Platform 服务来检测设备准备好的时机,所以我们将他注入构造器,然后在构造器中用它。
在此处我们不需要在构造器以外的地方用到 platform,但是在大部分的情况下,你需要在其他地方用到注入的服务。所以,为了在类里其他函数里可以访问到这些服务,我们必须将它设为成员变量。代码应该是这样的:
import { Component } from '@angular/core';
import { Platform } from 'ionic-angular';
@Component({
selector: 'home-page',
templateUrl: 'home.html'
})
export class HomePage {
someMemberVariable: any = “hey”!;
constructor(platform: Platform) {
this.platform = platform
}
someOtherMethod(){
this.platform.ready().then(() =>{
});
}
}
或者在TypeScript里面我们可以利用public关键字自动为他创建成员变量引用,如下:
import { Component } from '@angular/core';
import { Platform } from 'ionic-angular';
@Component({
selector: 'home-page',
templateUrl: 'home.html'
})
export class HomePage {
someMemberVariable: any = “hey”!;
constructor(public platform: Platform) {
}
someOtherMethod(){
this.platform.ready().then(() =>{
});
}
}
同时请注意任何定义在构造器上面的变量,如此处的someMemberVariable,都会自动设置为成员变量。所以,在本例中,我们可以随处使用this.someMemberVariable 来访问此变量。任何你想要用到的服务都需要注入(有的需要设置为成员变量)到构造器,任何需要用到的成员变量都需要定义在构造器上方。如果,当前这个观念对你来说有点迷糊,后续通过一些例子我们应该就会有感觉的。
现在,我们可以通过 this.platform 在任何地方访问 platform。如果我们没有设置这个成员变量然后通过调用 someOtherMethod 去访问 platform 的时候,将没啥用。
创建一个页面
你的应用永远会有大部分是页面 — 任何你想单独展示屏幕都将作为一个单独的 Page 定义。对之前而言,有一个特殊的装饰来完成,现在我们只用常规的 @Component。
我们之前讨论过,页面的类看起来应该是这样的:
import { Component } from '@angular/core';
@Component({
selector: 'home-page',
templateUrl: 'home.html'
})
export class MyPage {
constructor() {
}
}
这个装饰器里引用的模板大概是这样的:
<ion-header>
<ion-navbar>
<ion-title>
My Page
</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item>I</ion-item>
<ion-item>Am</ion-item>
<ion-item>A</ion-item>
<ion-item>List</ion-item>
</ion-list>
</ion-content>
模板文件组成了用户所见的东西(后续会更详细讨论模板)。模板文件和类协同工作:类定义了展示什么模板给用户,模板可以使用类里的数据和函数。
我们讲完了一个 Page 类的基本结构是什么样子的,以及 构造器 函数是干什么的,但是你也可以添加其他你的页面需要用到的函数,例如:
import { Component } from '@angular/core';
@Component({
selector: 'home-page',
templateUrl: 'home.html'
})
export class MyPage {
constructor() {
//this runs immediately
}
someMethod(){
//this only runs when called
}
someOtherMethod(){
//this only runs when called
}
}
你可以在构造器里面调用这些方法,或者你可以通过用户在模板中点击一个按钮来触发。这些附加方法可以添加到任何其他的类,他不是针对页面才能拥有的。稍后会详细讲到这些,目前我们只需要理解不同类类型之间结构的不同以及他们的作用就可以了。
创建一个组件
普通组件的代码看起来跟页面差不多(记住,页面就是一个组件)。当创建有组件的页面的时候,我们使用Ionic内置的导航来处理他们的显示。页面是一个霸占整个屏幕(即,用户的‘视窗’)的组件,但是一个组件允许创建你自定义的元素,然后用于插入到模板。有可能你需要创建一个自定义的日期选择器插入到页面中去,或者一个展示随机鸡汤文的框 —— 针对这样的需求你都可以创建一个自定义的 组件。
同样组件的类定义看起来差不多都是这样:
import { Component } from '@angular/core';
@Component({
selector: 'my-component',
templateUrl: 'my-component.html'
})
export class MyComponent {
text: any;
constructor() {
this.text = 'Hello World';
}
}
实际上此处组件唯一的不用是它指定了一个 selector。这就是你在将组件插入模板时用到的名字。即:
<my-component></my-component>
在考虑使用他之前,我们先看看组件的模板。这跟页面的模板没有任何区别。我们引用的模板文件名为 my-component.html 文件的内容是这样子的:
<div>
{{text}}
</div>
就跟页面一样,我们可以引用类定义里面存储的任何数据(函数也可以)。在这个模板中,我们的组件的工作是将下面内容渲染到DOM里去:
<div>Hello World</div>
确实很枯燥,但是你可以利用这个功能做很多有趣的,可重用的东西。现在,我们来看看如果在页面中使用这个组件。
你需要导入他然后加入到 app.module.ts 文件中:
@NgModule({
declarations: [
MyApp,
HomePage,
MyComponent
],
imports: [
IonicModule.forRoot(MyApp)
],
bootstrap: [IonicApp],
entryComponents: [
MyApp,
HomePage
],
providers: []
})
export class AppModule {}
然后在页面模板中直接这么用就可以了:
<my-component></my-component>
你后续基本上不需要创建这样的组件,因为Ionic以及提供了大部分你需要用到的组件(列表,标签页,按钮,输入框等等)。如果你需要的组件Ionic没有的话,那么你就可以考虑来看看怎么创建你自己的组件了。
注意:你可以在项目中通过 ionic g component MyComponent 命令来生成一个组件。
创建指令
之前也讲过,组件和指令非常像,但是总的来将组件是用来创建一个全新的元素,而指令是用于修改已存在组件的行为的。
自定义指令的类代码是这样的:
import { Directive } from '@angular/core';
@Directive({
selector: '[my-directive]'
})
export class MyDirective {
constructor() {
}
}
在这条指令中,我们有一个跟组件一样的 selector —— 但是又有少许不同。他不是用作标签名,而是作为元素的属性使用的。在Ionic 2中,你会经常用到这个,例如,在按钮上:
<button ion-button>
或者,列表上:
<ion-list no-lines>
在这里例子中我们创建的自定义指令可以在任何地方使用,如:
<button my-directive>
注意,这个指令实际上是没有模板的。尽管我们通常将应用中的任何特性都成为 ‘组件’,技术上讲组件是有一个类和一个模板(视图)组成 —— 如果他没有视图的话那么他就不是一个组件(他更像一个服务或者提供者)。
这里我普及点基础知识,我觉得需要知道 ElementRef。他使我们可以访问到添加了指令的元素上去。你可以在指令中添加如下:
import { Directive, ElementRef } from '@angular/core';
@Directive({
selector: '[my-directive]'
})
export class MyDirective {
constructor(element: ElementRef) {
this.element = element;
}
}
跟自定义组件一样他也需要去 app.module.ts 文件里面声明。
注意:可以通过命令 ionic g directive MyDirective 来生成一个新的 指令。
创建管道(Pipe)
第一眼看管道可能会有点复杂,但是实现真的很简单,他们看起来是这样的:
import { Injectable, Pipe } from '@angular/core';
@Pipe({
name: 'myPipe'
})
@Injectable()
export class MyPipe {
transform(value, args) {
//do something to 'value'
return value;
}
}
注意管道也是一个 @Injectable,我们大概的了解一下。他的理念是任何你传入管道的东西都将进入 tranform 函数,你可以在其中对值进行任何处理,然后返回新值。然后返回的值将会被渲染到屏幕上,而不是初始值。我们可以在模板中这样使用:
<p>{{someValue | myPipe}}</p>
不论 someValue 是什么他都会在展示之前经过自定义的管道处理。再次声明,使用自定义管道之前一定确保你已经在 app.module.ts 中导入和添加。
注意:可以通过命令 ionic g pipe MyPipe 来生成新的管道
创建注入(Injectable)
注入允许你创建一个服务在整个应用中使用(就像应用和外部或者网络数据服务之间的接口)。注入也可以作为哦哦‘提供者(Provider)’。Ionic CLI自动生成的@Injectable看起来是这样的:
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';
@Injectable()
export class Data {
data: any;
constructor(public http: Http) {
console.log('Hello Data Provider');
}
load() {
if (this.data) {
return Promise.resolve(this.data);
}
// don't have the data yet
return new Promise(resolve => {
// We're using Angular Http provider to request the data,
// then on the response it'll map the JSON data to a parsed JS object.
// Next we process the data and resolve the promise with the new data.
this.http.get('path/to/data.json')
.map(res => res.json())
.subscribe(data => {
// we've got back the raw data, now generate the core schedule data
// and save the data for later reference
this.data = data;
resolve(this.data);
});
});
}
}
以上代码创建了一个名为 Data 的提供者用于从JSON数据源(可以是一个本地的JSON文件,也可以是外部的JSON文件或者服务端响应)加载数据。他返回一个promise,promise允许在 http 请求执行完成之后获取数据。如果数据已经加载过了,那么他会直接返回数据(通过一个promise返回)。后续会深入了解如何通过 http 获取数据,目前我们只需要关注注入的基本知识。
所以,如果我想获取服务返回的数据,我们会在需要用到的类中注入他:
import { Component } from '@angular/core';
import { Data } from '../../providers/data';
@Component({
selector: 'home-page',
templateUrl: 'home.html'
})
export class MyPage {
constructor(public dataService: Data){
}
}
同时将他添加到 app.module.ts:
providers: [Data]
然后,你就可以在任何函数中通过 this.dataService来使用了,例如:
this.dataService.load().then((data) => {
console.log(data);
});
注意,我们这里用到了 then(),因为返回的是一个promise,所以我们需要等到promise完成才能访问数据。你可以考虑扩展一下提供一个save函数:
this.dataService.save(someData);
当然,提供者并不是专门用来获取数据的,你也可以用它做别的事情 —— 这个只是个使用比较广泛的用例而已。
注意:可以通过命令 ionic g provider MyProvider 来自动生成一个注入。
总结
我们广泛详尽地涉及了Ionic 2里面不同类型的类的创建,当然,还有更多需要我们去学习的。但是你现在知道得足够多了,在开始做例子的时候,所有东西看起来也不会那么奇怪与陌生了。