刚实习的时候用过AngularJS,那时候真的是连原生JavaScript都不会写,依样画葫芦做了几个管理后台。然后突然换项目了,AngularJS就不写了,感觉前前后后接触了一年多的AngularJS,结果只懂点皮毛。
最近又有个管理后台的需求,决定再拾起,但现在是升级版的Angular了。终于,有机会好好再看一眼Angular了,这次希望能深入一点了解。
本文是笔者在学习开发过程中的总结输出,目的在于让初次接触Angular的开发者对该框架能有整体的认识,并且能快速上手开发工作。
AngularJS最大版本号只有1.x,2.x/4.x的版本号都是针对于全新的框架Angular。但不能说Angular和AngularJS一点关系都没有,你看名字这么像,是吧?!回忆一下AngularJS被人念念不忘的特性,双向数据绑定,MVC,指令,服务,过滤器,模块化,脏检查机制,依赖注入,Scope,路由,表单校验等等。
看下AngularJS到Angular的过程中,哪些概念被保留下来,哪些被剔除了(所谓的取其精华,去其糟粕)。
剔除的部分:
保留/改善的部分:
新增的部分:
Angular团队为开发者提供了一个开箱即用(out of the box)的脚手架工具:Angular Cli。我们再也不用担心在项目初始化时,要搭建配置一系列的工具,比如webpack,karma,tslint,protractor等。
操作很简单,只要运行如下命令行就搞定了。
具体的语法教程可参考这里。
安装之后,文件目录如下:
my-dream-app
e2e // 端到端测试
app.e2e-spec.ts
app.po.ts
tsconfig.e2e.json
node_modules/... // npm包
src/... // 源码
angular-cli.json // 配置项
.editorconfig // 编辑器配置
.gitignore // git忽略文件配置
karma.conf.js // karma配置
package.json // npm配置
protractor.conf.js // 测试配置项
README.md // 项目说明
tsconfig.json // ts编辑器的配置
tslint.json // tslint配置项
我们需要关注的是src
文件夹,这里存放我们所有的源代码,开发的时候基本都在src
中。
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 // 全局css样式
test.ts // 单元测试主入口
Angular很重要的概念之一仍然是模块。Angular整个框架就是由很多个模块组成的,而不同的模块需要从不同的地方导入。打开package.json
文件,可以看到依赖的angular包可能是这样的:
"@angular/common": "^2.3.1",
"@angular/compiler": "^2.3.1",
"@angular/core": "^2.3.1",
"@angular/forms": "^2.3.1",
"@angular/http": "^2.3.1",
"@angular/platform-browser": "^2.3.1",
"@angular/platform-browser-dynamic": "^2.3.1",
"@angular/router": "^3.3.1",
来简单看下这些angular包中包含了哪些常用的模块(至少目前为止,我觉得常用的)。
以上模块都是Angular框架中的自带模块,而我们开发的完整单元也是模块。一个应用中至少要有一个模块,也就是根模块。 一些共享的功能属性我们可以抽象出来,成为共享模块。然后就是一些特性模块了。
模块的组成由组件,服务,指令,管道等等组成,这些概念会在下面讲到。定义模块的语法如下:
@NgModuel({
declarations: [], // 用到的组件,指令,管道
providers: [], // 依赖注入服务
imports: [], // 导入需要的模块
exports: [], // 导出的模块,跨模块交流
entryComponents: [] // 需提前编译好的模块
bootstrap: [] // 设置根组件
})
export class AppModule { }
所有用到的组件,指令,管道,模块都需要事先在模块中声明好,才能在具体组件中使用。服务可以在模块,组件,指令中的
providers
声明,也可以直接在运行时提供(参见Trotyl Yu的例子)。
一般情况下,在根模块的bootstrap
中设置启动的根组件即可,但也可以动态处理(参见Trotyl Yu的例子)。
那如何启动根模块呢?
在入口脚本中,也就是Angular Cli项目中的main.ts
中,启动如下:
// 导入需要模块
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
// 根模块
import { AppModule } from './app/app.module';
// 编译启动模块
platformBrowserDynamic().bootstrapModule(AppModule);
至此,我们对模块有所了解,也知道了模块的定义。
自从采用组件化的React大火之后,目前市面上炙手可热的框架全都采用了组件化的理念,Angular当然也不能落后了。可以说,组件化是Angular的核心理念。按Angular在中国的布道者大漠穷秋的话来说,就是:
Angular的核心概念是组件,模块化机制NgModule是为组件化服务的,实际上所有其它机制都是围绕组件化而来的。只有从组件化这个角度才能把握Angular的精神内核。
组件通常都是由模版和业务逻辑组成,看一下如何用Angular写一个很简单的组件:
// hello.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'hello',
template: ' {{greeting}}
',
styles: [`p { color: red;}`]
})
export class HelloComponent{
private greeting: string;
constructor(){
this.greeting = 'Hello, Angular2!';
}
}
// 使用
// 渲染结果
Hello, Angular2!
定义类HelloComponent
的时候,加上装饰器@Component
(Typescript语法),告诉Angular这个类是组件类。里面的数据称之为元数据(metadata),selector
属性说明了该组件对外的使用标记,template
就是组件的模版,styles
是组件的样式。而HelloComponent
中定义的就是该组件的业务逻辑了。
如果模版内容太多,可以单独写在一个html文件中,用templateUrl
属性引入;同理,样式文件用styleUrls
引入。
正如其他框架的组件,Angular的组件也是有生命周期这个概念。在不同的阶段不同的场景下,可以调用不同的生命周期函数钩子(hook)。
具体说明可以参考这里。
可以想像得到,组件化的页面结构最终会形成一颗组件树。盗一张Vue的图:
不可避免,我们需要考虑父子组件之间的参数传递问题。Anuglar提供的通信方式有如下几种:
// 父组件
import { Component } from '@angular/core';
@Component({
selector: 'hero-parent',
template: ` heroes </h2>
hero-child>
`
})
export class HeroParentComponent {
heroes = [{
name: 'John'
}, {
name: 'Lily'
}];
}
// 子组件
import { Component, Input } from '@angular/core';
import { Hero } from './hero';
@Component({
selector: 'hero-child',
template: `
{{hero.name}}</h3>
`
})
export class HeroChildComponent {
@Input() hero: Hero;
}
// 子组件
import { Component, EventEmitter, Output } from '@angular/core';
@Component({
selector: 'my-voter',
template: `
{{name}}
`
})
export class VoterComponent {
@Output() onVoted = new EventEmitter();
vote(agreed: boolean) {
this.onVoted.emit(agreed);
}
}
// 父组件
import { Component } from '@angular/core';
@Component({
selector: 'vote-taker',
template: `
Should mankind colonize the Universe?
Agree: {{agreed}}, Disagree: {{disagreed}}
"let voter of voters"
[name]="voter"
(onVoted)="onVoted($event)">
`
})
export class VoteTakerComponent {
agreed = 0;
disagreed = 0;
voters = ['Mr. IQ', 'Ms. Universe', 'Bombasto'];
onVoted(agreed: boolean) {
agreed ? this.agreed++ : this.disagreed++;
}
}
<h3>Countdown to Liftoff (via local variable)h3>
<button (click)="timer.start()">Startbutton>
<button (click)="timer.stop()">Stopbutton>
<div class="seconds">{{timer.seconds}}div>
<countdown-timer #timer>countdown-timer>
import { AfterViewInit, ViewChild } from '@angular/core';
import { Component } from '@angular/core';
import { CountdownTimerComponent } from './countdown-timer.component';
@Component({
selector: 'countdown-parent-vc',
template: `
Countdown to Liftoff (via ViewChild)
<div class="seconds">{{ seconds() }}div>
`
})
export class CountdownViewChildParentComponent implements AfterViewInit {
@ViewChild(CountdownTimerComponent)
private timerComponent: CountdownTimerComponent;
seconds() { return 0; }
ngAfterViewInit() {
setTimeout(() => this.seconds = () => this.timerComponent.seconds, 0);
}
start() { this.timerComponent.start(); }
stop() { this.timerComponent.stop(); }
}
模版说白了就是html的内容,常规的html基本都是静态内容,而模版结合了框架中的新语法使得html动态化。来看看Angular中的模版有什么便利的语法:
{{}}
我们可以看到上一节组件例子中的{{greeting}}
就是插值绑定。不仅可以获取变量的值,还可以直接写表达式。
value]='myData'>
还有其他的,比如样式绑定:
<div [ngClass]="{special: isSpecial}">div>
注意点:property和attribute不一样,想要绑定attribute,你需要写成property。比如:
Three-Four
你将会得到如下错误信息:
Template parse errors:
Can't bind to 'colspan' since it isn't a known native property
你需要改写成这样:
One-Two
// 或者
One-Two
<input (keyup)='handle($event)' >
可以是原生的事件:click,change,keydown,mousemove等,也可以是自定义事件,也可以是指令事件,比如ngSubmit
。
'data'>
// 双向绑定的背后其实是单向绑定和事件触发,等价于下面
"data" (ngModelChange)="data=$event">
注意点:使用ngModel,需要引入FormsModule模块。
还有些内置的指令:
可以在元素上用#或者ref-前缀来标记这个元素,然后在其他地方引用。
( )
<div *ngIf="show"> Can you see this? div>
如果还有else部分,可以如下操作:
Can you see this?
else block
+ *ngFor:循环
{{i}}: {{hero.name}}
具体的模版语法可以参考这里。
一个模块有了多个组件之后,需要用路由来配置哪个url呈现哪个组件。
首先,我们需要在入口页面的index.html中配置根路径:
...
...
...
然后创建一个路由模块:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
...
// 路由配置
const appRoutes: Routes = [
{ path: 'home', component: HomeComponent },
{ path: 'heroes', component: HeroesComponent },
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: '**', component: PageNotFoundComponent }
];
@NgModule({
imports: [
RouterModule.forRoot(appRoutes)
],
exports: [
RouterModule
]
})
export class AppRoutingModule {}
在主模块中导入配置好的路由模块:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
...
@NgModule({
imports: [
BrowserModule,
FormsModule,
AppRoutingModule
],
declarations: [
AppComponent,
HomeComponent,
HeroesComponent,
PageNotFoundComponent
],
bootstrap: [ AppComponent ]
})
export class AppModule { }
而在页面中需要一个容器
去承载:
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: `
Angular Router
`
})
export class AppComponent { }
上面代码中的routerLink
定义了用户点击后的路由跳转,routerLinkActive
定义该路由激活时的样式类。
路由上还可以带上一些索引参数:
{ path: 'heroes/:id', component: HeroesComponent },
获取的方式:
import { ActivatedRoute, Params } from '@angular/router';
...
export class a {
constructor(
private route: ActivatedRoute
) {}
// 路由参数
this.route.params
}
当模块很多,路由也很多的时候,我们可以使用模块懒加载的方式。懒加载的方式也很简单,在配置路由的时候修改如下即可:
const routes: Routes = [
{ // 默认转到订单管理
path: '',
redirectTo: '/order',
pathMatch: 'full'
},
{
path: 'order',
loadChildren: './order/order.module#OrderModule'
},
{
path: 'warehouse',
loadChildren: './warehouse/warehouse.module#WarehouseModule'
},
{
path: 'statistics/sales',
component: SalesComponent
}
];
// 在子模块中用RouterModule.forChild
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { OrderComponent } from './order.component';
const orderRoutes = [
{
path:'',
component: OrderComponent
}
];
@NgModule({
imports: [RouterModule.forChild(orderRoutes)],
exports: [RouterModule]
})
export class OrderRoutingModule {
}
服务是什么概念?可以简单地认为它是一个功能模块,重要在于它是单例对象,并且可以注入到其他的地方使用。
依赖注入是来自后端的概念,其实就是自动创建一个实例,省去每次需要手动创建的麻烦。
在Angular中定义一个服务很简单,主要在类之前加上@Injectable
装饰器的功能。这是最常见的依赖注入方式useClass,其他具体参见这里。
import { Injectable } from '@angular/core';
@Injectable()
export class Service {
counter: number = 0;
getData(){
return this.counter++;
}
}
然后在模块的providers
中声明:
import { Service } from './service';
...
@NgModule({
imports: [
...
],
declarations: [
...
],
providers: [ Service ], // 注入服务
bootstrap: [...]
})
export class AppModule {
}
使用的时候需要在构造器中建立关联:
import { Component } from '@angular/core';
import { Service } from './service';
...
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
constructor(public service: Service) {
// this.service被成功注入
// 相当于 this.service = new Service();
// 然后可以调用服务
this.service.getData();
}
}
由于该服务是在模块中注入,所以该模块中的所有组件使用这个服务时,使用的都是同一个实例。
除了在模块中声明,还可以在组件中声明。假设AppComponent
下还有组件HomeComponent
,此时我们在AppComponent
中注入这个服务:
import { Component } from '@angular/core';
import { Service } from './service';
...
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
providers: [ Service ], // 注入服务
})
export class AppComponent {
constructor(public service: Service) {
// this.service被成功注入
// 相当于 this.service = new Service();
// 然后可以调用服务
this.service.getData();
}
}
如果HomeComponent
也使用了这个服务,那它使用的将是同一个实例。这个可以从Service中的数据变化来看出。
Angular还有个分层依赖注入的概念,也就是说,你可以为任一组件创建自己独立的服务。就像上面的例子,如果想要HomeComponent
不和它的父组件同使用一个服务实例的话,只要在该组件中重新注入即可:
...
@Component({
selector: 'home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css'],
providers: [ Service ], // 重新注入服务
})
export class HomeComponent {
...
}
对于前后端的接口,通常会写成服务。下面说下请求后端数据这块应该怎么写。在模块这节中提过,http有专门的HttpModule
模块处理请求。首先要在模块中导入HttpModule
,然后引入http服务,调用相应的请求方法即可。
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/toPromise';
@Injectable()
export class HttpService {
constructor(private http: Http) {}
getFromServer():any {
return this.http.get(`/data`)
.toPromise()
.then(res => res.json())
.catch();
}
}
由于请求返回的对象是个可观察对象,可以转成Promise对象处理。这里需要用到RxJS的toPromise
操作符,然后用then
去处理返回成功结果,catch
处理失败情况。这样就搞定了后端数据的请求了。
RxJS又是另外一个比较高深的话题了,有机会深入学习一下再聊。
Angular的指令概念跟AngularJS的指令差不多,最重要的区别在于Angular中的组件继承指令,算是特殊的指令。我们看下用指令的方式去写组件的简单例子:
import { Directive,Input,ElementRef } from '@angular/core';
@Directive({
selector: 'hello'
})
export class HelloDirective {
@Input() name: string;
constructor(private el: ElementRef) {}
public ngOnInit(): void {
this.el.nativeElement.innerText = `hello ${this.name}!`;
}
}
// 使用组件指令
// 渲染结果
hello, Yecao!
不要忘记在使用前先在模块中声明哦,我觉得这是Angular最烦人的一点。
除此之外,还有属性指令和结构指令,属性指令只改变元素的样式或者行为。要写成属性指令,需要在selector
属性中用[]
包裹起来。来看简单地例子:
import { Directive, ElementRef, Renderer2 } from '@angular/core';
@Directive({
selector: '[highLight]'
})
export class HighLightDirective {
constructor(private el: ElementRef, private renderer2: Renderer2) { }
ngAfterViewInit() {
this.renderer2.addClass(this.el.nativeElement, 'highlight');
}
}
// 使用属性指令
这一段会高亮显示
结构指令就是模板中提到的*ngIf,*ngFor等指令,它修改了DOM结构。举个例子,重写*ngIf:
import { Directive, Input, ViewContainerRef, TemplateRef } from '@angular/core';
@Directive({
selector: '[myIf]'
})
export class MyIfDirective {
constructor(private templateRef: TemplateRef,
private viewContainer: ViewContainerRef) { }
@Input() set appMyIf(condition: boolean) {
if (condition) {
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
}
}
}
// 使用结构指令
这一段不会显示
管道其实就是过滤器,就是叫法不一致而已。主要用于格式化源数据,而不改变源数据。定义和使用的方式也很简单:
import { Pipe, PipeTransform } from '@angular/core';
/*
* 订单取消状态:默认为ALL表示全部,CANCEL表示已取消,NOTCANCEL表示正常
*/
@Pipe({ name: 'cancelStatus' })
export class CancelStatusPipe implements PipeTransform {
transform(status:string, blank: boolean):string {
const map = {
"ALL": "全部",
"NOTCANCEL": "正常",
"CANCEL": "已取消",
"": "暂无",
}
return blank? '特殊情况': map[status];
}
}
使用前记得在模块的declarations
声明,或者导到共享模块,在共享模块中导出去。使用如下:
{{ "ALL" | cancelStatus }} // 全部
{{ "ALL" | cancelStatus: true }} // 特殊情况
Angular内置了一些管道:
// 日期 DatePipe
{{ expression | date:"MM/dd/yy" }}
// 数字 DecimalPipe,digitInfo的组成 {minIntegerDigits}.{minFractionDigits}-{maxfractionDigits}
// minIntegerDigits:整数部分保留最小的位数,默认值为1.
// minFractionDigits:小数部分保留最小的位数,默认值为0.
// maxFractionDigits:小数部分保留最大的位数,默认值为3.
{{ expression | number[:digitInfo] }}
// 大写
{{ expression | uppercase }}
// 小写
{{ expression | lowercase }}
由于篇幅的限制,Angular的每个特性都点到为止,只是讲了一些基本概念和使用方法(我也只会这点而已),让你在项目中会用。还有一块项目中肯定会用到的是表单及其校验,这是个大头,还是放在下一篇单独拎出来说吧。
如果你看到了这里,谢谢你花了那么多时间阅读。最近刚淘了视频,出自这里。 跟大家分享一下,链接: http://pan.baidu.com/s/1c2CGkVY 密码: xwg6。
整体来说,接触Angular2不到一个月的时候,现在项目开发中。简单说下我的学习路径:
参考资料
本文首发于野草园,转载请注明出处。不当之处,欢迎批评指正!