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
ng new hello-angular
-
将
src/app/app.component.html
中内容删掉,只剩下router-outlet
-
新建一个组件
1
ng generate component first
-
编辑路由内容,在
src/app/app-routing.module.ts
中做如下修改1
2
3
4
5import { FirstComponent } from './first/first.component';
const routes: Routes = [
{path:'', component: FirstComponent},
]; -
启动项目
1
ng serve --open
-
看到打开的页面上写着 first works!
目录结构
1 | . |
重要概念
不了解重要概念一定不会用得轻松.这里进行一些简单的介绍.
官方给出的概念结构图.
元数据(metadata)
正如大多数有DI的框架一样,Angular也会对定义的类进行处理.
通常会使用符合需要的装饰器,
而这些装饰器里的各个字段组成的元数据,
会告诉Angular如何处理一个类.指导Angular的行为.
1 | // @符号和一个装饰器,Component表示这是一个组件 |
装饰器非常多,其中的元数据的字段和含义也是在用到的时候再解说更好理解.
组件(Component)
Angular中,功能以组件为单位进行划分,大到一个页面,小到页面上的一个列表,甚至列表中的每一个元素.都可以是组件,
通常组件包含
1 | login (组件文件夹) |
组件示例
1 | ({ |
模板(Template)
简单的html文件
1 | <h1>B component works</h1> |
为了安全,Angular会忽略<script>标记,并在控制台输出警告
数据绑定(data binding)
组件类中的定义的一些属性(成员变量),可以通过某些方式,绑定到模板文件中.
1 | <!-- 1.插值表达式 --> |
服务(Service)和依赖注入(dependency injection)
服务是向上层暴露结果的同时掩盖底层操作,
便于在换底层(比如从文件取数据改为从数据库取数据)时不需要改动上层,
特意分离出来的部分.
而有服务也通常会有降低耦合用的依赖注入.
1 | // 通常服务需要使用injectable装饰器 |
在组件中使用时则比较简单,构造函数中声明作变量即可
1 | constructor(private service: MessageService) { |
早一些的angular版本可能没有providedIn关键字,
那时的方法是在模块中为service的类声明一个key,然后在组件中使用key来注入对应service
1 | // 模块中 |
指令(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 | import { Directive, ElementRef } from '@angular/core'; |
使用时只需要让目标元素加上定义好的selector即可
1 | <p appHighlight>Highlight me!</p> |
自定义指令不光也可改变颜色等等,在一些模板驱动表单中,也可以完成一些字段的自定义验证(内容不能是特定短语等等)
模块(Module)
通常为了实现一个功能,会有一系列的组件,服务,路由等,这些东西放在一起就组成了一个模块.
比如有时登陆相关功能(用户注册,注销,登陆,改密码等)会放在一个模块中,与主要业务独立.
一个典型的模块可能包括
1 | ─ auth (模块目录) |
有时候还可以使用别人写的模块,比如如果想建立一个伪造的web服务器,去访问一个本地的json文件,
可以使用别人写的 InMemoryWebApiModule
等等.
模块定义本身的例子如下
1 | ({ |
管道(pipe)
常用在模板中,以 |
管道符的形式连接多个函数,以完成对最初变量的修改.
常用的用例如下
1 | <a>{{ value_expression | uppercase }} 大写</a> |
如果组件中有一个响应式的成员变量,也可以用async管道
1 | <a>{{hero$ | async}}</a> |
管道的用途多种多样
- 对单个元素做更改
- 对多个元素做过滤
- 发起http请求
- 缓存内容
等等等等.
甚至还可以自定义管道.比如这里实现对列表的过滤.
1 | name: 'flyingHeroes' }) ({ |
使用时
1 | <div *ngFor="let hero of (heroes | flyingHeroes)"> <!-- 在这里就完成了过滤 --> |
组件
组件的生命周期
- ngOnChnages 页面数据变化时触发,初始化时也会最先触发一次
- ngOnInit 组件的初始化,只执行一次
- ngDoCheck 数据发生了变化要验证
- ngAfterContentInit 页面内容的初始化,只执行一次
- ngAfterContentChecked 数据验证后跟着的内容验证?
- ngAfterViewInit 视图的初始化,只执行一次
- ngAfterViewChecked 内容验证后跟着视图的验证?
- ngOnDestroy 组件被销毁时调用
注意:
- ngOnChnages调用很频繁,不能放太多耗费性能的处理
- 如果页面上有ViewChild引用,则需要在ngAfterViewInit之后才能获取到真正的值
父子组件
许多场景下都有父子组件的使用例子.
比如一个代办事项的列表,除了列表本身对应的组件,也可能会将列表中的每个元素独立出来做成一个子组件.
此时就需要组件之间的互相通信,
比如父组件维护列表,而子组件拿到列表中的一个数据后开始渲染.
再比如子组件上触发了事件(单条的删除等等),需要通知父组件进行更改.
-
从父组件向子组件传递数据
通常在父组件的模板中使用变量绑定的方式传递变量数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import { Component } from '@angular/core';
import { HEROES } from './hero';
({
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
15import { Component, Input } from '@angular/core';
import { Hero } from './hero';
({
selector: 'app-hero-child',
template: `
<h3>{{hero.name}} says:</h3>
<p>I, {{hero.name}}, am at your service, {{masterName}}.</p>
`
})
export class HeroChildComponent {
// 这里的变量名要和父组件中的相同 () hero: Hero;
'master') masterName: string; // 如果不想相同,则需要重命名 (
}如果要在子组件中拦截父组件的值并做一些处理,可以这样做
1
2
3
4
5
6
7()
get name(): string { return this._name; }
set name(name: string) {
// 若父组件传来的值全是空格,则使用默认值
this._name = (name && name.trim()) || '<no name set>';
}
private _name=''; -
从子组件向父组件传递事件
子组件中使用
@Output
装饰器来声明一个需要向外传递的信号.
通常这个被传递的信号是一个由EventEmitter
包裹的值.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import { Component, EventEmitter, Input, Output } from '@angular/core';
({
selector: 'app-voter',
template: `
<button (click)="vote(true)" [disabled]="didVote">Agree</button>
<button (click)="vote(false)" [disabled]="didVote">Disagree</button>
`
})
export class VoterComponent {
new EventEmitter<boolean>(); () voted =
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
20import { Component } from '@angular/core';
({
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可供使用
不过通常一个简单的流程是
- ng new xxx 新建项目
- ng generate <something> 搭建一些简单的模板
- ng serve 启动项目
- ng test 运行测试
- 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库
-
将js文件放在合适的地方
-
在
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"
} -
在类中使用
1
2
3
4
5ngOnInit(): void {
const loader = (window as any).UnityLoader;
this.gameInstance = loader.someMethod();
}或者有更新的办法
1
2
3
4
5
6
7
8import { Component } from '@angular/core';
declare const someMethod: any; // 实现声明一个外部的常量,Angular会自动从外部库里查找的
export class UnityComponent implements OnInit {
onInit() {
someMethod(); // 如果是函数,则可以当作函数来使用
}
}
体会
官方的快速上手,是最方便了解angular基础的教程.
官方的英雄之旅(带危机中心)教程适合进阶了解angular.
其他妙用则需要自行探索.