概述
做过传统WEB开发的人,应该能够体会到网页里充斥着各种乱七八遭的脚本,比如一个jquery可能就有多个版本,再加上自己写的各种脚本,有时你都不知道一个功能是在哪个脚本里实现的。你只能Ctrol+Shift+F,来个全项目扫描,有时还能扫出多个实现。然后在JS中还有很多对dom的引用,你都不知道这些dom与在什么地方,实现的什么功能。
Angular对JS及页面进行了封装,那一个功能在一个组件就可以完成闭环,而不是东找西找。
Angular给了一个开发思路,比如使用Typescript减少语法错误,代码分层方便代码维护等等。
脚本、CSS及HTML分离
Angular中最小的可直接呈现的UI单元称之为组件,组件本身分为三个部分:
- HTML模板--HTML标签、点位符、控制指令,用于声明页面要渲染的内容。
- TypeScript类--表示组件的行为,存储数据
- CSS--组件的外观
通常用Angular cli命令生成的组件会包含三个对应的文件。
使用 Angular CLI 创建一个组件:
在终端窗口中,导航到要放置你应用的目录。
运行
ng generate component <component-name>
命令,其中<component-name>
是新组件的名字。默认情况下,该命令会创建以下内容:
一个以该组件命名的文件夹
一个组件文件
<component-name>.component.ts
一个模板文件
<component-name>.component.html
一个 CSS 文件,
<component-name>.component.css
测试文件
<component-name>.component.spec.ts
其中
<component-name>
是组件的名称。
比如为了呈现高德地图,创建了一个amap-container的组件,可以看到有三个文件,运行命令:
ng generate component
amap-container
我们把承载地图的标签放到.html文件里,地图创建的代码放到.ts里,容器的大小可以定义到.css文件里。
值得注意的是,一个组件是相对独立的模型。组件中定义的样式,并不会影响别的组件元素,同样ts中的代码也不会影响别的地方,这就是视图封装,组件和组件之间有着明显的边界,相互不影响。这样做的好处是,一方面方便维护代码维护,一个组件可以相对独立的业务逻辑单元,不需要费力处理整个页面程序的冲突问题;另一方面,可以方便团队协作,一个应用可以分派给多个人同时进行开发,相互之间不会造成冲突。
使用绑定分离示图与逻辑
传统的WEB开发中,我们通常直接用脚本控制dom元素来达到我们需要的动态效果。而Angurlar中将dom操作转移到了属性变化上,Angular不推荐你直接操作dom,而是使用绑定机制及控制指令对属性的变化做出反应。
对于绑定,我们可以参考官方的hero示例:
<h2>{{hero.name | uppercase}} Details</h2>
<div><span>id: </span>{{hero.id}}</div>
<div>
<label for="name">Hero name: </label>
<input id="name" [(ngModel)]="hero.name" placeholder="name">
</div>
当input的值在改变的时候,h2中的文本也跟着改变。可以看到,视图中仅仅标注了属性绑定,然后可以通过文本域改变属性的值,并没有做其它事情。那是不是在ts文件中做了什么操作吗?我们再下代码:
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
@Component({
selector: 'app-heroes',
templateUrl: './heroes.component.html',
styleUrls: ['./heroes.component.scss']
})
export class HeroesComponent implements OnInit {
hero: Hero = {
id: 1,
name: 'Windstorm'
};
constructor() { }
ngOnInit(): void {
}
}
可以看到, HeroesComponent也只定义了一个hero属性,也没做其它事情。这其中的奥秘,就是Angular框架在起作用。Angular会生成一个代理,用于监听属性值的变化,一但有变化就会通知视图中的做了绑定的部分,从而是界面发生变化。
通过绑定,我们还可以进行更复杂的视图控制,比如生成值列表,详细页的显示,我们把视图页改成这样:
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="let hero of heroes">
<button [class.selected]="hero === selectedHero" type="button" (click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span>
<span class="name">{{hero.name}}</span>
</button>
</li>
</ul>
<div *ngIf="selectedHero">
<h2>{{selectedHero.name | uppercase}} Details</h2>
<div>id: {{selectedHero.id}}</div>
<div>
<label for="hero-name">Hero name: </label>
<input id="hero-name" [(ngModel)]="selectedHero.name" placeholder="name">
</div>
</div>
这里头*ngFor是一个指令,用于迭代输出,可以根据属性数组生成相似的条目。*ngIf用于控制内容是否展示。可以看出,这里边也就是简单的循环操作和简单的条件判断,不过,这里多了一个事件绑定,因为按钮不能输入内容,但按钮可以触发事件,然后我们可以将事件通知告诉给ts文件。我们再看下ts文件:
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HEROES } from '../mock-heroes';
@Component({
selector: 'app-heroes',
templateUrl: './heroes.component.html',
styleUrls: ['./heroes.component.scss']
})
export class HeroesComponent implements OnInit {
heroes = HEROES;
selectedHero?: Hero;
constructor() { }
ngOnInit(): void {
}
onSelect(hero: Hero): void {
this.selectedHero = hero;
}
}
这里头也只定义了两个属性,一个heroes数组,一个selectedHero和一个onSelect事件,但在事件中,也只是返回选中的对象,并没有对dom进行任何控制,但视图上详细内容会跟着不同的按钮进行变换。
利用组件控制模块的粒度
假如一个页面既有导航,又有列表,还要展示详情页,为了避免程序的复杂度,尽量将代码拆成较小的模块,假如将所有代码写一个类里边,那结果可能变得不可控。我们把前边列表页中,用于显示详细内容的部分单独做成一个组件,然后再删除列表页中详细内容的部分,再引用新的组件,先看一下hello-detail的视图部分:
<div *ngIf="hero">
<h2>{{hero.name | uppercase}} Details</h2>
<div><span>id: </span>{{hero.id}}</div>
<div>
<label for="hero-name">Hero name: </label>
<input id="hero-name" [(ngModel)]="hero.name" placeholder="name">
</div>
</div>
再看下脚本部分:
import { Component, OnInit, Input } from '@angular/core';
import { Hero } from '../hero';
@Component({
selector: 'app-hero-detail',
templateUrl: './hero-detail.component.html',
styleUrls: ['./hero-detail.component.scss']
})
export class HeroDetailComponent implements OnInit {
@Input() hero?: Hero;
constructor() { }
ngOnInit(): void {
}
}
这里要说明的是,要在父组件的模板中对子控件进行赋值,需要加上@Input装饰器。@Input表明属性在模板中是否可配,以区分其它的属性。最后运行的效果,和前边一样。
利用依赖注入分离UI与业务逻辑
界面无关的内容,我们尽量将代码放到Service层中,比如我们向服务端请求一个数据集合,没必要在每个组件里去。比如定义这么一个Service:
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { Hero } from './hero';
import { HEROES } from './mock-heroes';
import { MessageService } from './message.service';
@Injectable({
providedIn: 'root',
})
export class HeroService {
constructor(private messageService: MessageService) { }
getHeroes(): Observable<Hero[]> {
const heroes = of(HEROES);
this.messageService.add('HeroService: fetched heroes');
return heroes;
}
}
上边加了这么一个装饰器:
@Injectable({
providedIn: 'root',
})
这样,就会生成一个全局的唯一实例。然后通过组件的构造函数注入:
constructor(private heroService: HeroService, private messageService: MessageService) { }
总结
框架在于提供一种开发规范,有了范式后,就可以很容易地对项目进行分割,规则,任务派发。