Angular一小时快速上手

Angular2是google在2014年9月发布全新的MVVM框架,由于不是基于AngularJS1.X简单升级后得到的,很多核心思想都不同甚至被抛弃。例:$scope的概念被抛弃,controller被抛弃,ES6的支持,TypeScript作为官方开发语言等等,所以全新的angular在命名规则上也有了一些变化,即:Angular。官方的寓意是Angular不在是一个简单的前端JS框架,他可以运行在任何在平台上,所以本文全部以官方名字为准。

准备工作

首先需要安装npm包管理工具,去官网下载对应自己系统的node和npm套件安装即可。

1
2
3
4
5
6
7
8
9
git clone https://github.com/angular/quickstart.git quickstart

npm install -g cnpm --registry=https://registry.npm.taobao.org

cd quickstart

cnpm install

npm start

此时浏览器中如果出现了‘Hello Angular’的字样,说明你已经成功的启动了第一个Angular项目,Good Luck。
注:如果已经安装过cnpm的同学可跳过第二步。

目录结构说明

文件说明
e2e端到端测试文件夹(可以忽略)
src项目主要资源文件夹
–/app/app资源文件夹
–/app/app.component.tsapp根组件逻辑文件(.js文件是编译后自己生成的,可忽略)
–/app/app.module.tsapp根模块
–/index.htmlapp入口文件
–/main.tsapp启动文件
–/styles.cssapp主要样式文件
–/tsconfig.jsontypescript编译配置文件
–/systemjs.config.jssystemjs模块加载配置文件
bs-config.e2e.json端到端测试基础配置文件(可以忽略)
bs-config.json基础配置文件(可以忽略)
karma.conf.js单元测试配置文件(可以忽略)
karma-test-shim.js单元测试配置文件(可以忽略)
package.jsonnpm配置文件
protractor.config.jsprotractor 端对端 (e2e) 测试器运行器的配置。
tslint.json该文件定义了 Angular 风格指南与本文档站作者喜爱的语法检查规则。

更多文件说明请查看:https://www.angular.cn/docs/ts/latest/guide/setup-systemjs-anatomy.html

IDE的选择

       因为TypeScript是官方推荐的Angular开发语言,而TypeScript又是Microsoft在2013年6月发布的一种开元的编程语言。TypeScript和ES6一样是JavaScript的超集,不同的是TypeScript同时还是ES6的超集。因为是C#之父主导开发的TypeScript,所以可想而知TypeScript相对JavaScript更像是一门面向对象的编程语言,同时支持静态类型、类的继承、多态、接口、命名空间、装饰器等特性。

       由于TypeScript面世时间不长,所以对应的IDE选择不是很多。首先推荐微软官方的VS code完全免费,且轻量级,对自家TypeScript最好的支持及语法提示,还有可视化的调试方案,丰富的插件可以安装,内部集成了命令行,可以快速的定位项目的文件夹,很方便。

       github官方出品的Atom 也是现在前端非常火的开发工具,丰富的插件可供安装,官方提供TypeScript语法提示插件。自己还可以安装汉化插件,主题视觉插件,其他的语法提示插件,类似less、sass、ES6、css、HTML等等。功能相对没有VS code强大但是相对比较轻量级,尤其是mac系统的同学如果已经有了主力开发工具,可以考虑用Atom作为辅助的开发工具。

       WebStorm 是老牌IDE厂商jetbrains公司旗下一款JavaScript 开发工具。在开发JavaScript代码项目的时候就体现出了强大的功能,开发TypeScript的项目一样需要安装插件来实现对TypeScript的语法提示及项目的构建。相对比较重量一些,因为内置了web服务器,不知道是否可以配置内部的服务器直接浏览TypeScript项目,如果可以的话应该非常方便,因为其他的IDE都需要使用node来起开发服务器,然后在浏览器中才可以看到构建好的项目。

       Sublime Text 这个应该很多人在用了,和Atom差不多,比较轻量级,没有内置服务器,可以安装插件。主题插件、提示插件等等,建议作为辅助开发工具或者文档查看工具。

       最后推荐一个APICloud Studio 2 ,这个是中国的公司开发的一款IDE。它基于刚才提到的Atom,在功能及界面上做了符合中国人的改进。界面全部原生汉语,不需要汉化,对于英文不好的同学可以考虑一手。另外开发中一些常用的功能,如代码的版本管理,它集成了SVN和GIT两种代码管理的方式。还有一些其他的非常好的功能,有兴趣的同学可以下去自己了解。

下载地址:
Atom: https://atom.io/
VS code:https://code.visualstudio.com/
WebStorm:http://www.jetbrains.com/webstorm/
ASublime Text: https://www.sublimetext.com/3
APICloud Studio 2: http://www.apicloud.com/devtools

了解代码

使用刚才下载的IDE打开./src/index.html,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html>
<head>
<title>Angular QuickStart</title>
<base href="/">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">

<!-- Polyfill(s) for older browsers -->
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('main.js').catch(function(err){ console.error(err); });
</script>
</head>
<body>
<my-app>Loading AppComponent content here ...</my-app>
</body>
</html>

代码说明:头部链接的js主要看system和system.config这两个js,其作用和requirejs和require.config.js基本一样。下面的<my-app></my-app>是app根组件的所在,也是整个app内容的入口。

接着打开./src/main.ts文件,代码如下。

1
2
3
4
5
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';

platformBrowserDynamic().bootstrapModule(AppModule);

代码说明:这个文件是app的引导文件,导入动态引导模块儿,然后启动app的跟模块AppModule即可。有静态引导和动态引导两种方式,区别在于浏览器编译和非浏览器编译,一般开发都使用动态引导即可,想了解的更深入的同学可以去查阅相关资料。

打开./src/app/app.module.ts文件,代码如下。

1
2
3
4
5
6
7
8
9
10
11
import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';

@NgModule({
imports: [ BrowserModule ],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }

代码说明:importexport@NgModule都是TypeScript的语法,import是载入一个文件,后面中括号中是载入文件后,对应文件输出的变量、方法、类等等。项目中需要的组件、模块、自定义的服务、第三方插件等等都需要用这种办法载入。以@开头的叫做装饰器,你可以理解为是一个组件或者模块的配置选项,里面是一些依赖、模板、样式等等。export字面理解就是输出、导出的意思,不管是模块、服务还是组件,想要让其他的文件访问到你本身的逻辑,一定要有导出,具体导出什么,怎么导出,根据业务需要可以不同,一般整个class导出即可。

打开./src/app/app.component.ts,你会看到以下的代码。

1
2
3
4
5
6
7
import { Component } from '@angular/core';

@Component({
selector: 'my-app',
template: `<h1>Hello {{name}}</h1>`,
})
export class AppComponent { name = 'Angular'; }

代码说明:
       如果是一个组件,必须在上面从angular核心模块儿中导入Component这个类,然后在装饰器@Component中配置你的组件。
       selector的意思是你组件的表现方式或者说是自定义标签是什么,这里要注意的地方是和AngularJS1.X版本的指令不太一样的地方是标签如果是’-‘连接的,组件中不需要使用驼峰命名法。
       template的意思是组件的HTML部分,可以是用多行引号,这是ES6的新特性,可以加载多行文本内容。templateUrl是加载外部模板的配置项,templatetemplateUrl不可以同时存在,否则会报错。
       styles是模板的内联样式,可以是css、less、sass。stylesUrl为模板指定外联样式文件,这里需要注意的是stylesstylesUrl可以同时存在。如果同时存在的话,styles会被先解析,然后是stylesUrl换句话说styles会被stylesUrl的样式覆盖。细心的同学可能注意到了stylesUrl是复数,所以stylesUrl的值可以是一个数组,类似['a.css','b.css']
       export class AppComponent这段是实现这个组件逻辑的主要区域,字面理解是一个类,熟悉java的同学应该不陌生。类的封装、继承及多态,它也一样都有。在类的主体中,定义了一个变量name,且它的值是Angular。此时我们再回头看template中的内容<h1>Hello </h1>,其中是插值表达式,用过js模板引擎、AngularJS1.X版本或者是vue的同学应该都不陌生。和AngularJS1.X不同的是在逻辑层我们不需要再把对应的变量或者方法绑定到某个angular暴露出来的变量或者方法上(类似AngularJS1.X的$scope$rootScope)。我们要做的就是名字对应上即可,剩下的交给Angular即可,框架会自动寻找组件对应的类进行匹配。

继续深入

官方示例扩展

继续刚才打开的./src/app/app.component.ts,我们先声明一个Hero的类,然后在AppComponent这个类中做一些改造,代码如下。

1
2
3
4
5
6
7
8
9
10
export class Hero {
id: number;
name: string;
}
export class AppComponent {
hero: Hero = {
id: 2,
name: 'Windstorm'
};
}

接着需要修改下template的内容。

1
<h1>{{hero.id}}</h1><h2>{{hero.name}} details!</h2>

浏览器会自动刷新,切换过去可以看下效果。

现在来点稍微复杂的功能,继续改造template这个类。

1
2
3
4
5
6
<h2>{{hero.name}} details!</h2>
<div><label>id: </label>{{hero.id}}</div>
<div>
<label>name: </label>
<input [(ngModel)]="hero.name" placeholder="name">
</div>

可以看到有输入框了,我们希望在input中输入内容,对应绑定的内容也变化,即双向绑定。双向绑定的写法是[(ngModel)]后面的值是要接收内容的变量名,下面我们来实现这个功能。

打开./src/app/app.module.ts,修改成如下的样子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms'; // <-- NgModel lives here
import { AppComponent } from './app.component';
@NgModule({
imports: [
BrowserModule,
FormsModule // <-- import the FormsModule before binding with [(ngModel)]
],
declarations: [
AppComponent
],
bootstrap: [ AppComponent ]
})
export class AppModule { }

可以看到相比修改之前,我们载入了FormsModule这个模块,然后在跟模块装饰器的imports配置项中加入了我们所依赖的FormsModule,这样就可以在整个app中使用FormsModule这个模块的所有功能了。这里可以理解为在项目启动时候,需要告诉Angular你需要用到哪些模块儿及组件。这时切换到浏览器,然后在输入框中随意输入,我们看到双向绑定的功能已经实现。

列表循环

./src/app/app.component.ts尾部添加如下代码。

1
2
3
4
5
6
7
8
9
10
11
12
const HEROES: Hero[] = [
{ id: 11, name: 'Mr. Nice' },
{ id: 12, name: 'Narco' },
{ id: 13, name: 'Bombasto' },
{ id: 14, name: 'Celeritas' },
{ id: 15, name: 'Magneta' },
{ id: 16, name: 'RubberMan' },
{ id: 17, name: 'Dynama' },
{ id: 18, name: 'Dr IQ' },
{ id: 19, name: 'Magma' },
{ id: 20, name: 'Tornado' }
];

接着在AppComponent的类中添加

1
heroes = HEROES;

然后修改我们模板下面加上列表的html代码

1
2
3
4
5
6
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="let hero of heroes">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>

给列表加上样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
styles: [`
.selected {
background-color: #CFD8DC !important;
color: white;
}
.heroes {
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 15em;
}
.heroes li {
cursor: pointer;
position: relative;
left: 0;
background-color: #EEE;
margin: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 4px;
}
.heroes li.selected:hover {
background-color: #BBD8DC !important;
color: white;
}
.heroes li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}
.heroes .text {
position: relative;
top: -3px;
}
.heroes .badge {
display: inline-block;
font-size: small;
color: white;
padding: 0.8em 0.7em 0 0.7em;
background-color: #607D8B;
line-height: 1em;
position: relative;
left: -1px;
top: -4px;
height: 1.8em;
margin-right: .8em;
border-radius: 4px 0 0 4px;
}`]

这时候切换到浏览器,已经可以看到一个带有样式的列表已经渲染好,为了保证我们的组件逻辑文件简介,我们把组件的样式单独拿出来形成单独的css文件,然后用外联的方式引入它。首先新建./app/app.css,然后修改AppComponent,删除原来的styles,添加新的属性,如下

1
styleUrls:['./app.css'],

处理事件

接着刚才列表循环的例子来做修改,在<li>标签上加上一下代码

1
(click)="onSelect(hero)"

写法类似AngularJS的事件绑定,但是不一样的是前面少了ng-,但是多了中括号的包裹。Angular中[]是属性绑定的写法,例[disabled]="choose"()是我们刚用到的事件绑定写法,还有之前用到的[(ngModel)]是双向绑定的写法。

AppComponent的类中添加

1
2
3
4
selectedHero: Hero;
onSelect(hero: Hero): void {
this.selectedHero = hero;
}

现在已经绑了<li>click事件,点击的逻辑可以在onSelect方法中饭实现。一般我们绑定事件以后可能会需要基于触发事件的详细信息来做一些逻辑,Angular提供了这样的方法,只需要在绑定事件后运行的方法中加入$event这个参数即可,代码如下。

1
(click)="onSelect(hero,$event)"

1
2
3
4
5
selectedHero: Hero;
onSelect(hero: Hero,e:any): void {
console.log(e);
this.selectedHero = hero;
}

修改后再次点击<li>我们会看到控制台中输出了

1
MouseEvent {isTrusted: true, screenX: 184, screenY: 381, clientX: 184, clientY: 291…}

这就是刚才点击事件的详细信息,想看全部内容的同学可以点开这个对象。

常用内置指令

ngIf

基于我们上一步的代码再做如下的修改

1
2
3
4
5
6
<h2>{{selectedHero.name}} details!</h2>
<div><label>id: </label>{{selectedHero.id}}</div>
<div>
<label>name: </label>
<input [(ngModel)]="selectedHero.name" placeholder="name"/>
</div>

这时我们切换到浏览器中,会发下如下的错误信息

1
EXCEPTION: TypeError: Cannot read property 'name' of undefined in [null]

原因是我们在AppComponent的类中只给selectedHero制定了类型而没有给它复制,然后在[(ngModel)]="selectedHero.name"这段代码中需要访问selectedHero中不存在的name属性,所以会报错,我们修改代码让程序运行起来。

1
2
3
4
5
6
7
8
<div *ngIf="selectedHero">
<h2>{{selectedHero.name}} details!</h2>
<div><label>id: </label>{{selectedHero.id}}</div>
<div>
<label>name: </label>
<input [(ngModel)]="selectedHero.name" placeholder="name"/>
</div>
</div>

       大家可以看到我们在刚才最初添加的代码外层又包了一个<div>,而且它的上面有一条指令*ngIf="selectedHero",表示如果selectedHero有值,才会显示<div>标签所包含的这段代码。这里注意下是有值,逻辑类似你在JavaScript中使用if(test)一个道理,如果test0nullundefined这类型的值,在if判断中test会被认为是false,所以需要注意下。
       这时切换到浏览器查看效果,发现报错没有了,而且只有当你点击了某个<li>后,上面有<input>标签的区域才会显示出来,这个就是ngIf指令的一般用法,注意别忘记前面的*

ngClass

       通常我们需要通过某个事件或者方法,改变视图层某个DOM的样式,最常用的方法就是给这个元素添加class。Angular提供这样的内置指令来帮助我们实现这样的功能,下面来继续改造我们的代码。

1
2
3
4
5
<li *ngFor="let hero of heroes"
[class.selected]="hero === selectedHero"
(click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>

       在<li>标签上添加[class.selected]="hero === selectedHero"这一条指令,意思是当selectedHero和当前<li>循环的hero相等时,添加classselected,切换到浏览器,点击某个<li>你已经可以看到效果。
       但是有时候,我们需要不同的事件或方法作用在同一个DOM上让它在不同状态的表现形式也不一样,这就需要添加多个class,当然你可以依照上面的思路添加多个[class.selected]指令也是可以的。但是Angular已经考虑到有这样的情况发生,为我们提供了更优美的解决方案。

1
[ngClass]="{'selected':hero === selectedHero,'normal':hero!=selectedHero}"

其中[ngClass] 表示一个 class 的对象集合,对象的 key 是我们需要操作的 classvalue 是这个 class 对应的表达式。

管道操作符 ( | )

在模板插值的时候我们最常用的应该就是管道操作符 ( | )这个了,Angular一样提供了一些内置的过滤器,常见的大小写、日期、货币、数字、百分比等等,当然也可以自定义过滤器。多个过滤器可以链式操作,管道的参数可以跟在这个管道的后面。

1
{{ birthday | date:'fullDate' | uppercase}}

这里再说一个比较常用的管道,在AngularJS1中官方内置了LimitTo的过滤器,但是Angular中并没有类似的过滤器,但是有一个slice的过滤器,使得我们操作数据更加灵活,用法和JavaScript原生的Array.slice一样。官方同样没有提供OrderByPipe这样的管道,需要的话都是需要自己写的。

1
2
3
4
5
<li *ngFor="let hero of heroes | slice:0:5"
[ngClass]="{'selected':hero === selectedHero,'normal':hero!=selectedHero}"
(click)="onSelect(hero,$event)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>

组件的嵌套

一个大型的应用是很多个组件嵌套组成的,Angular的组件嵌套实现起来非常简单,耦合程度也非常低,复用性高,我们把之前的代码进行修改便可以体=验一下。
我们在app这个目录下新建一个hero-detail.component.ts的文件,代码如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Component, Input } from '@angular/core';
import { Hero } from './hero';
@Component({
selector: 'hero-detail',
template: `
<div *ngIf="hero">
<h2>{{hero.name}} details!</h2>
<div><label>id: </label>{{hero.id}}</div>
<div>
<label>name: </label>
<input [(ngModel)]="hero.name" placeholder="name"/>
</div>
</div>
`
})
export class HeroDetailComponent {
@Input() hero: Hero;
}

我们的详情组件已经建好,需要在app.module.ts中配置一下即可使用,在头部添加

1
import { HeroDetailComponent } from './hero-detail.component';

然后在declarations中添加HeroDetailComponent

1
2
3
4
declarations: [
AppComponent,
HeroDetailComponent
],

因为列表组件和详情组件都用到了Hero这个class,所以我们把它剥离出来,在app目录下新建hero.ts

1
2
3
4
export class Hero {
id: number;
name: string;
}

最后我们要修改app.component.ts来调用我们刚才写好的详情组件。

1
2
3
4
5
6
7
8
9
10
11
template: `
<h1>{{title}}</h1>
<hero-detail [hero]="selectedHero"></hero-detail>
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="let hero of heroes | slice:0:5"
[ngClass]="{'selected':hero === selectedHero,'normal':hero!=selectedHero}"
(click)="onSelect(hero,$event)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>`,

在最上面引入Hero这个class,否则会报错。

1
import { Hero } from './hero';

然后切到浏览器,发现和以前没什么区别,但其实详情组件已经被载入进来,而且工作正常。
代码说明:详情组件中,比较陌生的是@Input(),组件之间通讯肯定会有输入和输出。刚才我们的详情组件,内容是由父组件提供,所以它需要接受父组件给他的数据,Angular组件接受数据使用@Input()。对应的有@Output(),我们这里不展开讲解,有兴趣的同学可以下去了解。

服务

       现在我们的应用已经初具规模,常见的功能也都体验过了。顺着上一步的思路我们继续想,组件的嵌套是为了提高组件的复用性、降低每个组件的耦合程度。组件是视图层面的一个个小的单元,那么逻辑层面可不可以有一些复用的东西形成单独的功能模块被组件或者其他的服务所调用呢?答案是可以的,类似于AngularJS1.x的各种服务servicefactory等等,Angular也可以自定义服务,但是种类不像AngularJS1.x版本分的那么详细,写法也相对简单了很多。
       接着上面的代码我们思考,列表页需要展示Heros,详情页需要展示hero,但是他们的数据源都是一样的。那么我们可不可以把数据源单独剥离出来形成一个服务呢?甚至我们可以通过后台来获取我们要显示的数据,下面我们就来改造代码。

首先新建app/app.service.ts文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { Injectable } from '@angular/core';

import { Hero } from './hero';

@Injectable()
export class AppService {
getHeroes(): Hero[] {
return HEROES;
}
}

const HEROES: Hero[] = [
{id: 11, name: 'Mr. Nice'},
{id: 12, name: 'Narco'},
{id: 13, name: 'Bombasto'},
{id: 14, name: 'Celeritas'},
{id: 15, name: 'Magneta'},
{id: 16, name: 'RubberMan'},
{id: 17, name: 'Dynama'},
{id: 18, name: 'Dr IQ'},
{id: 19, name: 'Magma'},
{id: 20, name: 'Tornado'}
];

需要注意的是,和新建组件不一样的地方是服务不需要引入Component这个类,但是需要Injectable这个类,所用是告诉Angular我们所写的服务中需要用到Angular的任何东西,Angular都会帮我们自动注入进来切一一对应好。
写好我们的服务以后,我们需要注入到我们的应用中即可使用,在app.module.ts中添加

1
import { AppService } from './app.service';

然后在@NgModule装饰器的对象中添加一个属性

1
providers: [ AppService ],

这一条的意思是我们的应用所依赖的服务,都可以在这里注入,而且在app.module.ts这个模块儿中注入的服务应用的所有组件都可以访问,所以一般会把一些全局的服务从这里注入,类似的有自己封装HTTP服务或者是整个应用的API服务等等。当然你也可以只在某个组件中注入这个组件所需要的服务,当父组件注入一个服务,该组件下的所有子组件都可以访问到这个服务。当子组件也需要注入这个服务,但是需要和父组件隔离或者需要不同的状态时,在子组件中再次注入这个服务,可以保证子组件注入的服务和父组件的服务相隔离。

然后回到我们的app.component.ts修改如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { Component,OnInit  } from '@angular/core';
import { Hero } from './hero';

import { AppService } from './app.service';

@Component({
selector: 'my-app',
styleUrls:['./app.css'],
template: `
<h1>{{title}}</h1>
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="let hero of heroes | slice:0:5"
[ngClass]="{'selected':hero === selectedHero,'normal':hero!=selectedHero}"
(click)="onSelect(hero,$event)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
`,
})
export class AppComponent implements OnInit {
title = 'Tour of Heroes';
heroes: Hero[];
selectedHero: Hero;
constructor(private appService: AppService) { }
getHeroes(): void {
this.heroes=this.appService.getHeroes();
}
ngOnInit(): void {
this.getHeroes();
}
onSelect(hero: Hero): void {
this.selectedHero = hero;
}
}

切到浏览器,我们已经可以看到列表已经渲染好了,说明我们的服务已经注入到应用中,并且我们在组件中成功的调用了它。因为我们的数据现在是模拟的,都是同步加载,但是实际情况中,我们展示的数据往往来自服务器,所以都是异步的操作,这时需要我们引入一个Promise的概念,熟悉ES6的同学都应该不陌生,我们引入“承诺与异步编程”的概念来解决这个问题,修改app.service.ts代码如下

1
2
3
getHeroes(): Promise < Hero[] > {
return Promise.resolve(HEROES);
}

这样一个简单的模拟服务器返回数据的获取数据的方法已经写好了,对应的调用此方法的地方也需要调整一下,修改app.component.ts代码如下

1
2
3
getHeroes(): void {
this.heroService.getHeroes().then(heroes => this.heroes = heroes);
}

这里用到了ES6的箭头函数,可以优雅的处理 this 指针。上面箭头函数等同于下面的代码

1
2
3
4
5
6
7
8
getHeroes(): void {
let that = this;
this.heroService.getHeroes().then(
function(heroes){
that.heroes = heroes;
}
);
}

这里粗略的讲一下箭头函数,更详细的可以去读一下“阮一峰”的ECMAScript 6 入门,这里就不展开讲了。
修改完成以后,我们切到浏览器,发现列表也是正常渲染的,说明刚才的服务已经正常工作了。

路由

       路由对于一个SPA(单页应用)来说是必不可少的,Angular给我们提供了相对于AngfularJS1.x更为便捷的路由服务,下面我们就来实现一个简单的路由功能。
       想要体验路由功能,最少需要两个页面,所以我们把英雄列表和英雄详情拿出来每个都形成单独的组件,在./app下新建hero-list.component.tshero-detail.component.ts,然后把app.component.ts中对应的每个组件的代码复制到响应组件的.ts文件中,最终代码如下

app.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Component, OnInit } from '@angular/core';

@Component({
selector: 'my-app',
template: `
<h1>{{title}}</h1>
<router-outlet></router-outlet>
`,
})
export class AppComponent implements OnInit {
title = 'Tour of Heroes';
constructor() {}
ngOnInit(): void {}
}

这里需要注意的是<router-outlet></router-outlet>这个标签,这个标签的意思是告诉Angular路由切换的内容需要呈现在哪里。

hero-list.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { Component } from '@angular/core';
import { Router } from '@angular/router';

import { AppService } from './app.service';

import { Hero } from './hero';

@Component({
selector: 'hero-list',
template: `
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="let hero of heroes | slice:0:5"
[ngClass]="{'selected':hero === selectedHero,'normal':hero!=selectedHero}"
(click)="onSelect(hero,$event)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
`
})
export class HeroListComponent {
heroes:any;
constructor(
private appService: AppService,
private router: Router
) {}
getHeroes(): void {
this.appService.getHeroes().then(heroes => this.heroes = heroes);
}
ngOnInit(): void {
this.getHeroes();
}
onSelect(hero: Hero,e:any): void {
this.router.navigate(['/detail', hero.id]);
}
}

这里需要注意的是需要引入路由服务,因为我们要从列表页导航到详情页面,当然你可以使用最原始的办法:window.location.href这样的方式来实现,需要自己去拼接字符串。

hero-detail.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { Location } from '@angular/common';

import { AppService } from './app.service';

import { Hero } from './hero';

@Component({
selector: 'hero-detail',
template: `
<div *ngIf="hero">
<h2>{{hero.name}} details!</h2>
<div><label>id: </label>{{hero.id}}</div>
<div>
<label>name: </label>
<input [(ngModel)]="hero.name" placeholder="name"/>
</div>
<button (click)="goBack()">Back</button>
</div>
`
})
export class HeroDetailComponent implements OnInit {
hero: any;
constructor(
private appService: AppService,
private route: ActivatedRoute,
private location: Location
) {}
ngOnInit(): void {
this.getHeroe(this.route.params);
}
getHeroe(obj:any): void {
this.appService.getHeroes(obj).then(hero => this.hero = hero);
}
goBack(): void {
this.location.back();
}
}

详情页需要引入import { ActivatedRoute, Params } from '@angular/router'import { Location }from '@angular/common';一个是获取URL参数的服务,一个是返回列表页要用到。
基本工作我们已经搞定,然后需要在app.module.ts配置我们的路由表来实现路由的功能。在顶部引入路由模块

1
import { RouterModule }   from '@angular/router';

还有刚才新建的两个组件

1
2
import { HeroListComponent }  from './hero-list.component';
import { HeroDetailComponent } from './hero-detail.component';

然后在对应位置把他们注入到我们的应用中即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@NgModule({
imports: [
BrowserModule,
FormsModule,
RouterModule.forRoot([
{
path: '',
redirectTo: '/heroes',
pathMatch: 'full'
},
{
path: 'heroes',
component: HeroListComponent
},
{
path: 'detail/:id',
component: HeroDetailComponent
}
])
],
declarations: [
AppComponent,HeroListComponent,HeroDetailComponent
],
providers: [ AppService ],
bootstrap: [ AppComponent]
})

这时路由的功能已经实现,但是我们发现到详情页的时候会报错,因为我们刚才的服务是单独为列表页写的,如果详情页和列表页公用一个服务的话,需要我们稍作改动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
getHeroes(obj?: any): Promise < Hero[] > {
let id = obj?obj.value.id:undefined;
let result:any;
if(id===undefined){
result = HEROES;
}{
HEROES.forEach(function(e:any){
if(e.id==id){
result = e;
}
});
}
return Promise.resolve(result);
}

       修改完成后,应用已经可以正常运行,可以点击英雄到详情页,然后再点击back按钮返回列表页,路由功能已经实现。然而我们发现列表的样式没有了,打开开发者工具查看,发现Angular会把我们的样式自动隔离在某个组件中,实现单个组件的样式只服务于这个组件本身,所以会造成路由过来的内容是无法享受到app.component.ts这个组件下的样式。所以我们需要把app.css添加到index.html中作为全局样式,这样任何组件都可被渲染到。

Http

       任何一个需要与服务端通讯的客户端应用都缺少不了与服务端通讯的服务,有的是自己封装的,有的是通过插件实现的,也有的是框架提供的。相比AngularJS1.x,Angular为我们提供了更为完善,功能更为强大的HTTP服务,下面我们来初步体验一下。
       在./app/下新建service目录,然后在service目录下新建appHttp.tsappApi.ts,代码如下

appHttp.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import { Injectable } from '@angular/core';
import { Http, RequestOptions, Headers,URLSearchParams } from '@angular/http';
import 'rxjs/add/operator/map';

const baseUrl = 'http://api.jisuapi.com/car';
const appKey = 'c115b803134d35f2';

@Injectable()
export class appHttp {
constructor(private http: Http) {}
request(url: string,
opts: any) {
console.log(opts);
return this.http.request(url, new RequestOptions(opts)).map(res = > {
console.log(res);
return res.json();
})
};
post(url: string,
opts ? : Object) {
let params = new URLSearchParams();
params.set('appkey', appKey);
console.log(params);
let body = JSON.stringify({
appkey: appKey
});
let headers = new Headers({
'Content-Type': 'application/json'
});
let options = new RequestOptions({
body: body,
headers: headers,
method: 'post'
});
return this.request(baseUrl + url, ( < any > Object).assign(options, opts));
};
get(url: string,
opts ? : Object) {
console.log(opts);
let params = new URLSearchParams();
params.set('appkey', appKey);
let body: any = {
search: params
}
console.log(params);
return this.request(baseUrl + url, ( < any > Object).assign(body, opts));
};

}

appApi.ts

1
2
3
4
5
6
7
8
9
10
11
import { Injectable } from '@angular/core';
import { appHttp } from './appHttp';


@Injectable()
export class appApi{
constructor(private _http: appHttp) {}
getCar(){
return this._http.get('/brand');
}
}

然后修改我们的app.module.ts,在顶部引入我们刚才所写的两个服务并且注入到我们的应用中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { HttpModule } from "@angular/http";
import { appHttp } from './service/appHttp';
import { appApi } from './service/appApi';

imports:[
...
HttpModule,
...
],
providers:[
...
appHttp,
appApi
]

这时我们自定义的http服务已经注入到应用中,可以在应用下的任何组件使用,让我们来修改app.component.ts,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { Component, OnInit } from '@angular/core';
import { appApi } from './service/appApi';

@Component({
selector: 'my-app',
template: `
<h1>{{title}}</h1>
<router-outlet></router-outlet>
`,
})
export class AppComponent implements OnInit {
title = 'Tour of Heroes';
private cars:any;
constructor(private api:appApi) {}
getCar(){
this.api.getCar().subscribe(data => {
this.cars = data;
console.log(data);
});
}
ngOnInit(): void {
this.getCar();
}
}

切换到浏览器,打开开发者工具,刷新页面,可以看到控制台已经打印出从服务端返回的数据,说明我们刚才自定义的http服务已经正常工作。Angular的http服务功能非常强大,这里就不展开讲解了,想了解的可以下去自己到官网查看示例和API文档。

6、整合第三方插件

我们在开发应用的过程中,往往需要借助第三方插件来实现某些功能。最常见的就是各种jquery插件,还有echarts图标插件、moment日期操作插件、datepicker日期插件、swiper滑块插件等等。在以往的项目中,我们用过最原始的方式<script>标签来载入我们需要的插件,也用过requireJS、seaJS、CommonJS这样前端模块化的解决方案,还有AngularJS1.x时期通过依赖注入的方式。但是因为Angular使用编程语言的特殊性,以往的引入方式都无法正常使用插件或者不是最好的解决方案,下面我们来大致介绍下Angular中引入第三方插件的方法。
首先使用cnpm来安装我们需要的插件

1
2
3
cnpm install -S jquery
cnpm install -s moment
....

然后在app.component.ts顶部引入我们刚才安装的插件,在Oninit方法中添加一些测试代码来验证插件是否正常工作。

1
2
3
4
5
6
7
...
import * as $ from 'jquery';
import * as moment from 'moment';
...
console.log($);
onsole.log(moment().subtract(6, 'days').format('YYYY MM DD'));
...

然后切换到浏览器,发现控制台提示我们jquery找不到,服务器404错误代码,这时候我们需要在systemjs.config.js中配置一下我们刚才添加的插件,类似requireJS的config文件,需要现配置一下,然后在项目中使用。在systemjs.config.jsmap字段中添加

1
2
3
4
...
'jquery':'npm:jquery/dist/jquery.min.js',
'moment': 'npm:moment/moment.js'
...

       保存以后刷新浏览器,发现已经不报错,而且控制台已经打印出jquery的$和moment的方法,说明第三方插件已经成功加载。
       因为Angular项目的特殊性,搭建开发环境有很多种方案,现在主要的方式有angular-cli、angular-seed、system.js这几种,我们使用的官方的这个quickstart的例子就是system.js方式,其他两种开发环境引入第三方插件的方式大同小异,有兴趣的同学可以深入了解下,本文不展开讲解。

注:如果最初package.json中依赖没有jquery,后续安装的话,虽然可以运行,再次npm start时可能会报错,需要cd到你项目tsconfig.json所在的文件夹下,执行以下命令来解决报错。

1
2
3
cnpm install typings -g

typings install dt~jquery --global --save

这是因为jquery没有指定任何类型,不符合typescript的语法标准,需要安装一个插件来帮助我们在typescript的项目中使用JavaScript写的插件。

坚持技术分享,您的支持将鼓励我继续创作!
7* WeChat Pay

微信打赏

7* Alipay

支付宝打赏