Angular2笔记

背景

为了考hacker rank上的angular测试,看了Angular的教程.
(尽管如今hacker rank的web测试已经开始各种崩溃,几乎不能用了.)
这里记录一些入门的,概念性的东西.

前提知识

  • 带类型的js: typescript
  • 流式编程思想: rxjs

简介

谷歌出品,跨平台的前端库,目前不如React火.

特点

  • 类库官方包办,减少纠结
  • RxJS友好
  • 支持NativeScript和ReactNative做原生开发
  • 支持服务端渲染
  • 拥有CLI工具,可以快速搭建应用(创建新工程,启动工程,编译等)

历史

早期有AngularJS,通常的版本是1.x,后来计划重构成为2.0版本,不过难产了.
Angular本身算是一个独立的产品,可能复用了一部分计划在AngularJs2.0中的想法,
Angualr支持多种语言,typescript,dart,不过js的支持官方已经不再说了.
但人们可能依然将AngularJs对应至1.x版本,而将Angular称为2.0版本.
Angular目前最新版本已经到11,但或许概念上和2相差不多.

Hello World

  1. 建立一个新的项目

    1
    ng new hello-angular
  2. src/app/app.component.html 中内容删掉,只剩下 router-outlet

  3. 新建一个组件

    1
    ng generate component first
  4. 编辑路由内容,在 src/app/app-routing.module.ts 中做如下修改

    1
    2
    3
    4
    5
    import { FirstComponent } from './first/first.component';

    const routes: Routes = [
    {path:'', component: FirstComponent},
    ];
  5. 启动项目

    1
    ng serve --open
  6. 看到打开的页面上写着 first works!

目录结构

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
.
├── angular.json 项目配置文件
├── e2e 端到端测试文件
│ ├── protractor.conf.js
│ ├── src
│ │ ├── app.e2e-spec.ts
│ │ └── app.po.ts
│ └── tsconfig.json
├── karma.conf.js karma配置
├── package-lock.json
├── package.json js项目的配置文件
├── README.md
├── src
│ ├── app 处在第0层的"app"组件
│ │ ├── app.component.css app组件的view层
│ │ ├── app.component.html app组件的view层
│ │ ├── app.component.spec.ts 一些测试代码
│ │ ├── app.component.ts 基础的,外露的app组件
│ │ ├── app.module.ts 根模块文件
│ │ ├── app-routing.module.ts 路由功能文件,不知道为什么独立成了一个模块
│ │ └── first 处在第1层的first组件文件夹
│ │ ├── first.component.css
│ │ ├── first.component.html
│ │ ├── first.component.spec.ts
│ │ └── first.component.ts
│ ├── assets 存放一些图标等
│ ├── environments 有关环境的配置文件
│ │ ├── environment.prod.ts
│ │ └── environment.ts
│ ├── favicon.ico
│ ├── index.html 首页
│ ├── main.ts 功能引导
│ ├── polyfills.ts
│ ├── styles.css
│ └── test.ts
├── tsconfig.app.json typescript配置文件
├── tsconfig.json
├── tsconfig.spec.json
└── tslint.json 语法检查配置文件

重要概念

不了解重要概念一定不会用得轻松.这里进行一些简单的介绍.
官方给出的概念结构图.

元数据(metadata)

正如大多数有DI的框架一样,Angular也会对定义的类进行处理.
通常会使用符合需要的装饰器,
而这些装饰器里的各个字段组成的元数据,
会告诉Angular如何处理一个类.指导Angular的行为.

1
2
3
4
5
6
7
8
9
// @符号和一个装饰器,Component表示这是一个组件
@Component({
selector: 'hero-list', // 这些字段就是元数据
templateUrl: './hero-list.component.html',
providers: [ HeroService ]
})
export class HeroListComponent implements OnInit {
/* 该类被认为是组件 */
}

装饰器非常多,其中的元数据的字段和含义也是在用到的时候再解说更好理解.

组件(Component)

Angular中,功能以组件为单位进行划分,大到一个页面,小到页面上的一个列表,甚至列表中的每一个元素.都可以是组件,
通常组件包含

1
2
3
4
5
login (组件文件夹)
├── login.component.css (css类)
├── login.component.html (模板文件)
├── login.component.spec.ts (测试文件)
└── login.component.ts (组件类,写一些逻辑)

组件示例

1
2
3
4
5
6
7
8
@Component({
selector: 'hero-list', // 在Angular中如何引用该组件
templateUrl: './hero-list.component.html', // 该组件的html页面定义位置
providers: [ HeroService ] // 该组件会用到哪些服务
})
export class HeroListComponent implements OnInit {
/* 该类被认为是组件 */
}

模板(Template)

简单的html文件

1
2
3
4
<h1>B component works</h1>

可以使用selector来引用另外的一些组件
<hero-list></hero-list>

为了安全,Angular会忽略<script>标记,并在控制台输出警告

数据绑定(data binding)

组件类中的定义的一些属性(成员变量),可以通过某些方式,绑定到模板文件中.

1
2
3
4
5
6
7
8
9
10
11
<!-- 1.插值表达式 -->
<li>{{hero.name}}</li>

<!-- 2.属性绑定 把父组件selectedHero的值传到子组件的hero属性中 -->
<hero-detail [hero]="selectedHero"></hero-detail>

<!-- 3. 事件绑定 点击父组件时调用子组件的selectHero方法 -->
<li (click)="selectHero(hero)"></li>

<!-- 4. 双向绑定 数据属性值通过属性绑定从组件流到输入框.用户的修改通过事件绑定流回组件,把属性值设置为最新的值 -->
<input [(ngModel)]="hero.name">

服务(Service)和依赖注入(dependency injection)

服务是向上层暴露结果的同时掩盖底层操作,
便于在换底层(比如从文件取数据改为从数据库取数据)时不需要改动上层,
特意分离出来的部分.
而有服务也通常会有降低耦合用的依赖注入.

1
2
3
4
5
6
7
// 通常服务需要使用injectable装饰器
@Injectable({
providedIn: 'root', // 此处指定一个注入器,不过暂时不用管有什么影响
})
export class MessageService {
someMethod():any {}
}

在组件中使用时则比较简单,构造函数中声明作变量即可

1
2
3
constructor(private service: MessageService) {
service.someMethod();
}

早一些的angular版本可能没有providedIn关键字,
那时的方法是在模块中为service的类声明一个key,然后在组件中使用key来注入对应service

1
2
3
4
5
6
7
8
9
10
11
// 模块中
@NgModule({
providers: [
{ provide: 'auth', useClass: AuthService }
]
})

// 组件中
constructor(
@Inject('auth') private service, // 那时的angular还没有严格的类型检查
private router: Router) { }

指令(Directive)

除了常见的直接写html,现代的网页可能会倾向于使用js动态生成一些html结构.
指令可以看作这种行为的一个快捷方式.不知道为什么这样取名,只知道大约分为三类

  • 组件
    一种非常特殊以至于需要独立出来的指令

  • 结构型指令
    会改变html的DOM结构,比如 ngIf, ngFor

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <p *ngIf="!sending">
    <!-- 使用ngIf控制p元素的显示,当sending为false,即并非在传送信息时,才显示send和cancel按钮 -->
    <button (click)="send()">Send</button>
    <button (click)="cancel()">Cancel</button>
    </p>

    <ul>
    <!-- 使用ngFor控制li的重复,结果会产生多个li元素,同时还可以指定index的变量名 -->
    <li *ngFor="let hero of heroes; let i = index">{{i}}.{{hero.name}}</li>
    </ul>
  • 属性型指令
    可以改变DOM节点的属性,比如 ngClass, NgStyle, ngModel (这里把form的模型也看成了节点的一种属性)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <!-- 当组件中的isSpecial变量变为true时,div元素的class中则增加一条定义名为special -->
    <!-- 此处仅仅是一个示范,如果只更改一个class,通常会用属性绑定 -->
    <div [ngClass]="isSpecial ? 'special' : ''">This div is special</div>

    <!-- 在其他地方定义好了css类,并且是可以由代码控制的
    this.currentStyles = {
    'font-style': this.canSave ? 'italic' : 'normal',
    'font-weight': !this.isUnchanged ? 'bold' : 'normal',
    'font-size': this.isSpecial ? '24px' : '12px'
    };
    -->
    <div [ngStyle]="currentStyles">
    This div is initially italic, normal weight, and extra large (24px).
    </div>

    <!-- ngModel通常用在表单的双向绑定中 -->
    <label for="example-ngModel">[(ngModel)]:</label>
    <input [(ngModel)]="currentItem.name" id="example-ngModel">

不过也可以自定义指令:

1
2
3
4
5
6
7
8
9
10
11
import { Directive, ElementRef } from '@angular/core';

@Directive({
selector: '[appHighlight]' // 该selector要用在被调用的地方
})
export class HighlightDirective {
constructor(el: ElementRef) {
// 此处的指令作用是: 改变元素的背景色
el.nativeElement.style.backgroundColor = 'yellow';
}
}

使用时只需要让目标元素加上定义好的selector即可

1
<p appHighlight>Highlight me!</p>

自定义指令不光也可改变颜色等等,在一些模板驱动表单中,也可以完成一些字段的自定义验证(内容不能是特定短语等等)

模块(Module)

通常为了实现一个功能,会有一系列的组件,服务,路由等,这些东西放在一起就组成了一个模块.
比如有时登陆相关功能(用户注册,注销,登陆,改密码等)会放在一个模块中,与主要业务独立.
一个典型的模块可能包括

1
2
3
4
5
6
7
8
9
─ auth (模块目录)
├── auth-routing.module.ts (模块同目录下的独立路由模块)
├── auth.guard.ts (模块中的其他文件,此处是一个守卫)
├── auth.module.ts (模块定义本身)
├── auth.service.ts (模块中的一些service)
└── login (模块中的组件)
├── login.component.css
├── login.component.html
└── login.component.ts

有时候还可以使用别人写的模块,比如如果想建立一个伪造的web服务器,去访问一个本地的json文件,
可以使用别人写的 InMemoryWebApiModule 等等.
模块定义本身的例子如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@NgModule({
imports: [ // 需要加载的一些其他模块
BrowserModule,
BrowserAnimationsModule,
FormsModule, // Angular的模块
HeroesModule, // 自定义的Heroes模块
AuthModule, // 自定义的Auth模块
AppRoutingModule,// app模块本身的路由模块,为防止变量未定义等问题,放在最后
],
declarations: [ // 该模块中定义的一些组件等等
AppComponent,
ComposeMessageComponent,
PageNotFoundComponent
],
bootstrap: [ AppComponent ] // 将哪个组件作为默认显示的组件
})
export class AppModule {}

管道(pipe)

常用在模板中,以 | 管道符的形式连接多个函数,以完成对最初变量的修改.
常用的用例如下

1
2
3
<a>{{ value_expression | uppercase }} 大写</a>
<a>The time is {{today | date:'h:mm a z'}} 转为本地化日期</a>
<a>{{birthday | date | uppercase}} 串联来写</a>

如果组件中有一个响应式的成员变量,也可以用async管道

1
<a>{{hero$ | async}}</a>

管道的用途多种多样

  1. 对单个元素做更改
  2. 对多个元素做过滤
  3. 发起http请求
  4. 缓存内容

等等等等.

甚至还可以自定义管道.比如这里实现对列表的过滤.

1
2
3
4
5
6
7
8
@Pipe({ name: 'flyingHeroes' })
export class FlyingHeroesPipe implements PipeTransform {
// 自定义的管道需要实现pipeTransform接口的transform函数
transform(allHeroes: Flyer[]) {
// 功能是过滤英雄列表中,canFly属性为true的结果
return allHeroes.filter(hero => hero.canFly);
}
}

使用时

1
2
3
<div *ngFor="let hero of (heroes | flyingHeroes)"> <!-- 在这里就完成了过滤 -->
{{hero.name}}
</div>

组件

组件的生命周期

  1. ngOnChnages 页面数据变化时触发,初始化时也会最先触发一次
  2. ngOnInit 组件的初始化,只执行一次
  3. ngDoCheck 数据发生了变化要验证
  4. ngAfterContentInit 页面内容的初始化,只执行一次
  5. ngAfterContentChecked 数据验证后跟着的内容验证?
  6. ngAfterViewInit 视图的初始化,只执行一次
  7. ngAfterViewChecked 内容验证后跟着视图的验证?
  8. ngOnDestroy 组件被销毁时调用

注意:

  • ngOnChnages调用很频繁,不能放太多耗费性能的处理
  • 如果页面上有ViewChild引用,则需要在ngAfterViewInit之后才能获取到真正的值

父子组件

许多场景下都有父子组件的使用例子.
比如一个代办事项的列表,除了列表本身对应的组件,也可能会将列表中的每个元素独立出来做成一个子组件.
此时就需要组件之间的互相通信,
比如父组件维护列表,而子组件拿到列表中的一个数据后开始渲染.
再比如子组件上触发了事件(单条的删除等等),需要通知父组件进行更改.

  1. 从父组件向子组件传递数据

    通常在父组件的模板中使用变量绑定的方式传递变量数据

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

    import { HEROES } from './hero';

    @Component({
    selector: 'app-hero-parent',
    template: `
    <h2>{{master}} controls {{heroes.length}} heroes</h2>
    <app-hero-child *ngFor="let hero of heroes" // 父组件自己维护列表
    [hero]="hero" // 左侧是子组件中的变量名,右侧是父组件循环中的变量
    [master]="master">
    </app-hero-child>
    `
    })
    export class HeroParentComponent {
    heroes = HEROES;
    master = 'Master';
    }

    然后在子组件中使用 @Input 装饰器来声明一个外来的数据

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

    import { Hero } from './hero';

    @Component({
    selector: 'app-hero-child',
    template: `
    <h3>{{hero.name}} says:</h3>
    <p>I, {{hero.name}}, am at your service, {{masterName}}.</p>
    `
    })
    export class HeroChildComponent {
    @Input() hero: Hero; // 这里的变量名要和父组件中的相同
    @Input('master') masterName: string; // 如果不想相同,则需要重命名
    }

    如果要在子组件中拦截父组件的值并做一些处理,可以这样做

    1
    2
    3
    4
    5
    6
    7
    @Input()
    get name(): string { return this._name; }
    set name(name: string) {
    // 若父组件传来的值全是空格,则使用默认值
    this._name = (name && name.trim()) || '<no name set>';
    }
    private _name='';
  2. 从子组件向父组件传递事件

    子组件中使用 @Output 装饰器来声明一个需要向外传递的信号.
    通常这个被传递的信号是一个由 EventEmitter 包裹的值.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import { Component, EventEmitter, Input, Output } from '@angular/core';

    @Component({
    selector: 'app-voter',
    template: `
    <button (click)="vote(true)" [disabled]="didVote">Agree</button>
    <button (click)="vote(false)" [disabled]="didVote">Disagree</button>
    `
    })
    export class VoterComponent {
    @Output() voted = new EventEmitter<boolean>();
    didVote = false;

    // 子组件的click事件会调用vote函数
    vote(agreed: boolean) {
    // 向父组件发射信号,emit中会为"瓶子"内部装上值
    this.voted.emit(agreed);
    this.didVote = true;
    }
    }

    父组件中则要接收该事件,并进行进一步的处理

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

    @Component({
    selector: 'app-vote-taker',
    template: `
    <app-voter *ngFor="let voter of voters" [name]="voter" (voted)="onVoted($event)">
    圆括号里的voted要和子组件中声明的一致
    然后父组件就要调用自己的onVoted函数
    </app-voter>
    `
    })
    export class VoteTakerComponent {
    agreed = 0;
    disagreed = 0;

    // 参数类型就是"瓶子"里装的值的类型
    onVoted(agreed: boolean) {
    agreed ? this.agreed++ : this.disagreed++;
    }
    }

CLI的使用

有大片的cli可供使用
不过通常一个简单的流程是

  1. ng new xxx 新建项目
  2. ng generate <something> 搭建一些简单的模板
  3. ng serve 启动项目
  4. ng test 运行测试
  5. ng build 打包

同时也有npm的传统,可以使用缩写来代表全称
ng generate component 可以使用 ng g c 来代替.

此处着重于generate说一说

1
ng generate component login --inline-template --inline-style
  • inline-template 表示不会生成单独的html文件,会放在元数据中
  • inline-style 表示不会生成单独的css文件,会放在元数据中
1
ng generate module my-module --routing
  • routing 表示会带上路由一同创建,具体表现是,
    在建成的my-module模块文件夹下会有一个 my-module-routing.module.ts 文件

如果在建立模块时忘了为其添加路由,可以这样添加

1
ng generate module app-routing --module app --flat
  • flat 表示不会为该模块新建文件夹(通常这就是路由模块了)
  • module 表示该模块(路由模块)会放在app模块的文件夹下,同时还会注册在app模块中

CLI将 src/app/ 作为根路径,因此默认创建出的文件位于 src/app/ 下,
如果指定路径path,就会创建在 src/app/path/

1
ng generate component crisis-center/crisis-list

src/app/crisis-center/ 下创建crisis-list组件.
而组件的存在形式通常是一个文件夹.
再比如

1
ng generate guard auth/auth

会在 src/app/auth/ 文件夹下生成一个 auth.guard.ts 文件

部署

使用 ng build --prod 打包应用,
然后会在 dist 文件夹下生成一些打包后的文件,比如 index.html
然后放在nginx配置好的路径下即可.

剑走偏锋的操作

引用外部js库

  1. 将js文件放在合适的地方

  2. angular.json 中注册

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    {
    "projects": {
    "xxx": {
    "architect": {
    "build": {
    "options": {
    "scripts": [
    "src/assets/Build/UnityLoader.js"
    ]
    }
    }
    }
    }
    },
    "defaultProject": "xxx"
    }
  3. 在类中使用

    1
    2
    3
    4
    5
    ngOnInit(): void {
    const loader = (window as any).UnityLoader;

    this.gameInstance = loader.someMethod();
    }

    或者有更新的办法

    1
    2
    3
    4
    5
    6
    7
    8
    import { Component } from '@angular/core';
    declare const someMethod: any; // 实现声明一个外部的常量,Angular会自动从外部库里查找的

    export class UnityComponent implements OnInit {
    onInit() {
    someMethod(); // 如果是函数,则可以当作函数来使用
    }
    }

体会

官方的快速上手,是最方便了解angular基础的教程.
官方的英雄之旅(带危机中心)教程适合进阶了解angular.
其他妙用则需要自行探索.

参考

  1. Angualr的历史
  2. 目录结构官方说明
  3. 概念讲解