angular 入门

一、安装

1.安装@angular/cli

cnpm i -g @angular/cli

2.使用脚手架工具创建项目

ng new angular-demo

3.安装依赖

cnpm install || cnpm i

4.启动项目

ng server --port 5000 --open
  1. 创建新组件
ng generate component NewComponentName

6.创建模块

ng generate module 模块名称

7.创建服务

ng generate service 服务名称

8.创建模型

ng generate class 类名称

二、指令

内置属性指令

NgClass  --添加和删除一组css类
NgStyle  --添加和删除一组HTML样式
NgModel  --将数据双向绑定添加到HTML表单元素

NgClass --添加和删除一组css类

<div [ngClass]="isSpecial ? 'special' : ''">This div is special</div>

NgStyle --添加和删除一组HTML样式



<div [ngClass]="currentClasses">This div is initially saveable, unchanged, and special.</div>

NgModel --将数据双向绑定添加到HTML表单元素

<label for="example-ngModel">[(ngModel)]:</label>
<input [(ngModel)]="currentItem.name" id="example-ngModel">

内置结构性指令

NgIf      --从模板中创建或销毁子视图
NgFor     --为列表中的每个条目重复渲染一个节点
NgSwitch  --组在备用试图之间切换的指令

NgIf --从模板中创建或销毁子视图

<app-item-detail *ngIf="isActive" [item]="item"></app-item-detail>

NgFor --为列表中的每个条目重复渲染一个节点

<app-item-detail *ngFor="let item of items" [item]="item"></app-item-detail>

NgSwitch --组在备用试图之间切换的指令

<div [ngSwitch]="currentItem.feature">
  <app-stout-item    *ngSwitchCase="'stout'"    [item]="currentItem"></app-stout-item>
  <app-device-item   *ngSwitchCase="'slim'"     [item]="currentItem"></app-device-item>
  <app-lost-item     *ngSwitchCase="'vintage'"  [item]="currentItem"></app-lost-item>
  <app-best-item     *ngSwitchCase="'bright'"   [item]="currentItem"></app-best-item>
<!-- . . . -->
  <app-unknown-item  *ngSwitchDefault           [item]="currentItem"></app-unknown-item>
</div>

三、依赖注入

当某个服务依赖于另一个服务是,请遵与注入组件相同的模式

例如:将logger.service注入到此组件中

import { Injectable } from '@angular/core';
import { HEROES } from './mock-heroes';
import { Logger } from '../logger.service';

@Injectable({
  providedIn: 'root',
})
export class HeroService {

  constructor(private logger: Logger) {  }

  getHeroes() {
    this.logger.log('Getting heroes ...');
    return HEROES;
  }
}
logger.service
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class Logger {
  logs: string[] = []; // capture logs for testing

  log(message: string) {
    this.logs.push(message);
    console.log(message);
  }
}

四、模板

1.文本插值

<h3>Current customer: {{ currentCustomer }}</h3>

你不能使用那些具有或可能引发副作用的 JavaScript 表达式,包括:

  • 赋值 (=, +=, -=, ...)
  • 运算符,比如 newtypeofinstanceof 等。
  • 使用 ;, 串联起来的表达式
  • 自增和自减运算符:++--
  • 一些 ES2015+ 版本的运算符

和 JavaScript 语法的其它显著差异包括:

2.模板语句

<button (click)="deleteHero()">Delete hero</button>

不允许使用以下 JavaScript 和模板表达式语法:

  • new

  • 递增和递减运算符 ++--

  • 赋值运算符,例如 +=-=

  • 按位运算符,例如 |&

  • 管道

3.管道

​ 管道用来对字符串、货币金额、日期和其他显示数据进行转换和格式化。管道是一些简单的函数,可以在模板表达式中用来接受输入值并返回一个转换后的值

​ 要应用管道,请如下所示在模板表达式中使用管道操作符(|),紧接着是该管道的名字,对于内置的 DatePipe 它的名字是 date

  
<p>The hero's birthday is {{ birthday | date }}</p>

4.属性绑定

​ Angular 中的属性绑定可帮助你设置 HTML 元素或指令的属性值。使用属性绑定,你可以执行诸如切换按钮、以编程方式设置路径,以及在组件之间共享值之类的功能。

<img [src]="itemImageUrl">

5.属性、类、样式绑定

绑定属性: colspan
 <tr><td [attr.colspan]="1 + 1">One-Two</td></tr>

绑定样式
  <nav [style.background-color]="expression"></nav>
  <nav [style.backgroundColor]="expression"></nav>

绑定类
  
  <div [class]="classExpression"></div>  

6.事件绑定

<button (click)="onSave()">Save</button>

7.双向绑定

双向绑定为应用中的组件提供了一种共享数据的方式。使用双向绑定绑定来侦听事件并在父组件和子组件之间同步更新值。

​ 为了使双向数据绑定有效,@Output() 属性的名字必须遵循 inputChange 模式,其中 input 是相应 @Input() 属性的名字。例如,如果 @Input() 属性为 size ,则 @Output() 属性必须为 sizeChange

后面的 sizerComponent 具有值属性 size 和事件属性 sizeChangesize 属性是 @Input(),因此数据可以流入 sizerComponentsizeChange 事件是一个 @Output() ,它允许数据从 sizerComponent 流出到父组件。

接下来,有两个方法, dec() 用于减小字体大小, inc() 用于增大字体大小。这两种方法使用 resize() 在最小/最大值的约束内更改 size 属性的值,并发出带有新 size 值的事件。

// sizer.component.ts
export class SizerComponent {

  @Input()  size!: number | string;
  @Output() sizeChange = new EventEmitter<number>();

  dec() { this.resize(-1); }
  inc() { this.resize(+1); }

  resize(delta: number) {
    this.size = Math.min(40, Math.max(8, +this.size + delta));
    this.sizeChange.emit(this.size);
  }
}
// sizer.component.html
 <div>
  <button (click)="dec()" title="smaller">-</button>
  <button (click)="inc()" title="bigger">+</button>
  <label [style.font-size.px]="size">FontSize: {{size}}px</label>
</div>
// app.component.html
<app-sizer [size]="fontSizePx" (sizeChange)="fontSizePx=$event"></app-sizer>

8.模板变量

<input #phone placeholder="phone number" />

9.输入与输出

父子传值

<parent-component>
  <child-component></child-component>
</parent-component>

子组件:

要使用 @Input() 装饰器,首先要导入 Input,然后用 @Input() 装饰该属性

import { Component, Input } from '@angular/core'; // First, import Input
export class ItemDetailComponent {
  @Input() item = ''; // decorate the property with @Input()
}

<p>
  Today's item: {{item}}
</p>

通过settting截停输入属性值的变化:

import { Component, Input } from '@angular/core'; // First, import Input
export class ItemDetailComponent {
   @Input()
  get item(): string { return this._item; }
  set item(item: string) {
    this._item = (item && item.trim()) || '<no item set>';
  }
  private _item = '';
}

<p>
  Today's item: {{item}}
</p>

通过ngOnchanges()来截听输入属性值的变化:截听多个值时比setting合适

import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';

@Component({
  selector: 'app-version-child',
  template: `
    <h3>Version {{major}}.{{minor}}</h3>
    <h4>Change log:</h4>
    <ul>
      <li *ngFor="let change of changeLog">{{change}}</li>
    </ul>
  `
})
export class VersionChildComponent implements OnChanges {
  @Input() major = 0;
  @Input() minor = 0;
  changeLog: string[] = [];

  ngOnChanges(changes: SimpleChanges) {
    const log: string[] = [];
    for (const propName in changes) {
      const changedProp = changes[propName];
      const to = JSON.stringify(changedProp.currentValue);
      if (changedProp.isFirstChange()) {
        log.push(`Initial value of ${propName} set to ${to}`);
      } else {
        const from = JSON.stringify(changedProp.previousValue);
        log.push(`${propName} changed from ${from} to ${to}`);
      }
    }
    this.changeLog.push(log.join(', '));
  }
}

父组件:

<app-item-detail [item]="currentItem"></app-item-detail>

export class AppComponent {
  currentItem = 'Television';
}

子父通信

子组件:

import { Output, EventEmitter } from '@angular/core';

export class ItemOutputComponent {

  @Output() newItemEvent = new EventEmitter<string>();

  addNewItem(value: string) {
    this.newItemEvent.emit(value);
  }
}


<label for="item-input">Add an item:</label>
<input type="text" id="item-input" #newItem>
<button (click)="addNewItem(newItem.value)">Add to parent's list</button>

父组件:

export class AppComponent {
  items = ['item1', 'item2', 'item3', 'item4'];

  addItem(newItem: string) {
    this.items.push(newItem);
  }
}

<app-item-output (newItemEvent)="addItem($event)"></app-item-output>

五、组件

1.生命周期
ngOnChanges()    
 用途: 当 Angular 设置或重新设置数据绑定的输入属性时响应。 该方法接受当前和上一属性值的 SimpleChanges 对象
 时机: 在 ngOnInit() 之前以及所绑定的一个或多个输入属性的值发生变化时都会调用。
 注意,如果你的组件没有输入,或者你使用它时没有提供任何输入,那么框架就不会调用 ngOnChanges()。
 
ngOnInit()
 用途:在 Angular 第一次显示数据绑定和设置指令/组件的输入属性之后,初始化指令/组件。 欲知详情,参阅本文档中的初始化组件或指令。
 时机: 在第一轮 ngOnChanges() 完成之后调用,只调用一次。

ngDoCheck()
 用途:检测,并在发生 Angular 无法或不愿意自己检测的变化时作出反应。 欲知详情和范例,参阅本文档中的自定义变更检测。
 时机:紧跟在每次执行变更检测时的 ngOnChanges() 和 首次执行变更检测时的 ngOnInit() 后调用。

ngAfterContentInit()
 用途:当 Angular 把外部内容投影进组件视图或指令所在的视图之后调用。
 时机: 第一次 ngDoCheck() 之后调用,只调用一次。
 
ngAfterContentChecked()
用途:每当 Angular 检查完被投影到组件或指令中的内容之后调用。
时机:ngAfterContentInit() 和每次 ngDoCheck() 之后调用

ngAfterViewInit()
用途:当 Angular 初始化完组件视图及其子视图或包含该指令的视图之后调用。
时机:第一次 ngAfterContentChecked() 之后调用,只调用一次。

ngAfterViewChecked()
用途:每当 Angular 做完组件视图和子视图或包含该指令的视图的变更检测之后调用。
时机:ngAfterViewInit() 和每次 ngAfterContentChecked() 之后调用

ngOnDestroy()
用途:每当 Angular 每次销毁指令/组件之前调用并清扫。 在这儿反订阅可观察对象和分离事件处理器,以防内存泄漏。
时机:在 Angular 销毁指令或组件之前立即调用。
2.内容投影
a.单插槽内容投影

容投影的最基本形式是单插槽内容投影。单插槽内容投影是指创建一个组件,你可以在其中投影一个组件。

要创建使用单插槽内容投影的组件,请执行以下操作:

  1. 创建一个组件。
  2. 在组件模板中,添加 ng-content 元素,让你希望投影的内容出现在其中。
import { Component } from '@angular/core';

@Component({
  selector: 'app-zippy-basic',
  template: `
  <h2>Single-slot content projection</h2>
  <ng-content></ng-content>
`
})
export class ZippyBasicComponent {}

有了 ng-content 元素,该组件的用户现在可以将自己的消息投影到该组件中。ng-content 元素是一个占位符,它不会创建真正的 DOM 元素。ng-content 的那些自定义属性将被忽略。

<app-zippy-basic>
  <p>Is content projection cool?</p>
</app-zippy-basic>
b.多插槽内容投影

一个组件可以具有多个插槽。每个插槽可以指定一个 CSS 选择器,该选择器会决定将哪些内容放入该插槽。该模式称为多插槽内容投影。使用此模式,你必须指定希望投影内容出现在的位置。你可以通过使用 ng-contentselect 属性来完成此任务。

要创建使用多插槽内容投影的组件,请执行以下操作:

  1. 创建一个组件。

  2. 在组件模板中,添加 ng-content 元素,让你希望投影的内容出现在其中。

  3. select 属性添加到 ng-content 元素。 Angular 使用的选择器支持标签名、属性、CSS 类和 :not 伪类的任意组合。

import { Component } from '@angular/core';

@Component({
  selector: 'app-zippy-multislot',
  template: `
  <h2>Multi-slot content projection</h2>
  <ng-content></ng-content>
  <ng-content select="[question]"></ng-content>
`
})
export class ZippyMultislotComponent {}

使用 question 属性的内容将投影到带有 select=[question] 属性的 ng-content 元素。

<app-zippy-multislot>
  <p question>
    Is content projection cool?
  </p>
  <p>Let's learn about content projection!</p>
</app-zippy-multislot>

如果你的组件包含不带 select 属性的 ng-content 元素,则该实例将接收所有与其他 ng-content 元素都不匹配的投影组件。

在前面的示例中,只有第二个 ng-content 元素定义了 select 属性。结果,第一个 ng-content 就会元素接收投影到组件中的任何其他内容。

c.有条件的内容投影

如果你的组件需要有条件地渲染内容或多次渲染内容,则应配置该组件以接受一个 ng-template 元素,其中包含要有条件渲染的内容。

​ 在这种情况下,不建议使用 ng-content 元素,因为只要组件的使用者提供了内容,即使该组件从未定义 ng-content 元素或该 ng-content 元素位于 ngIf 语句的内部,该内容也总会被初始化。

使用 ng-template 元素,你可以让组件根据你想要的任何条件显式渲染内容,并可以进行多次渲染。在显式渲染 ng-template 元素之前,Angular 不会初始化该元素的内容。

ng-template 进行条件内容投影的典型实现。

  1. 创建一个组件。
  2. 在接受 ng-template 元素的组件中,使用 ng-container 元素渲染该模板,例如
<ng-container [ngTemplateOutlet]="content.templateRef"> </ng-container>

3.将 ng-container 元素包装在另一个元素(例如 div 元素)中,然后应用条件逻辑。

<div *ngIf="expanded" [id]="contentId">
    <ng-container [ngTemplateOutlet]="content.templateRef"> </ng-container>
</div>

4.在要投影内容的模板中,将投影的内容包装在 ng-template 元素中

<ng-template appExampleZippyContent>
  It depends on what you do with it.
</ng-template>

5.创建一个带有与这个模板的自定义属性相匹配的选择器指令。在此指令中,注入 TemplateRef 实例

@Directive({
  selector: '[appExampleZippyContent]'
})
export class ZippyContentDirective {
  constructor(public templateRef: TemplateRef<unknown>) {}
}

6.在你要将内容投影到的组件中,使用 @ContentChild 获取此投影内容的模板。

@ContentChild(ZippyContentDirective) content!: ZippyContentDirective;
3.动态组件

组件模板不会永远是固定的,应用可能会需要在运行期间加载一些新的组件。

动态组件加载

1.指令

在添加组件之前,先定义一个锚点来告诉angular要把组件插入到什么地方

mport { Directive, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[adHost]',
})
export class AdDirective {
  constructor(public viewContainerRef: ViewContainerRef) { }
}

AdDirctive注入了ViewContainerRef来获取容器视图的访问权,这个容器就是那些动态加入的组件的宿主

在@Directive装饰器中,要注意选择器的名称:ad-host,它就是你将应用到元素上的指令

2.加载组件

元素就是刚才制作的指令将应用到的地方。

template: `
            <div class="ad-banner-example">
              <h3>Advertisements</h3>
              <ng-template adHost></ng-template>
            </div>

元素是动态加载组件的最佳选择,因为它不会渲染任何额外的输出。

六、路由

1.创建路由
app-routing.module.ts

//routes 路由数组中加入你要创建的路由

const routes: Routes = [
  { path: 'first-component', component: FirstComponent },
  { path: 'second-component', component: SecondComponent },
];

路由顺序

路由的顺序很重要,因为router在匹配时使用“先到先得”策略,所以应该在不那么具体的路由前面放置更具体的路由,首先列出静态路径的路由,然后是一个默认路由匹配的空路径路由,通配符理由是最后一个,因为他匹配每一个URL,只有当其他路由都没有匹配时,Router才会选择它。

2、获取路由信息

使用ActivatedRoute接口从路由中获取信息:

import { Router, ActivatedRoute, ParamMap } from '@angular/router';、

constructor(
  private route: ActivatedRoute,
) {}

ngOnInit() {
  this.route.queryParams.subscribe(params => {
    this.name = params['name'];
  });
}
3.设置通配符路由
{ path: '**', component:  }
4.显示404页面
const routes: Routes = [
  { path: 'first-component', component: FirstComponent },
  { path: 'second-component', component: SecondComponent },
  { path: '**', component: PageNotFoundComponent },  // Wildcard route for a 404 page
];
5.设置重定向
const routes: Routes = [
  { path: '',   redirectTo: '/first-component', pathMatch: 'full' }, // redirect to `first-component`
];
6.嵌套路由
const routes: Routes = [
  {
    path: 'first-component',
    component: FirstComponent, // this is the component with the <router-outlet> in the template
    children: [
      {
        path: 'child-a', // child route path
        component: ChildAComponent, // child route component that the router renders
      },
      {
        path: 'child-b',
        component: ChildBComponent, // another child route component that the router renders
      },
    ],
  },
];
7.使用相对路径
<nav>
  <ul>
    <li><a routerLink="../second-component">Relative Route to second component</a></li>
  </ul>
</nav>
8.指定相对路由
goToItems() {
  this.router.navigate(['items'], { relativeTo: this.route });
}
9.获取路由参数
import { ActivatedRoute } from '@angular/router';

constructor(private route: ActivatedRoute) {}

ngOnInit() {
  const heroId = this.route.snapshot.paramMap.get('id');
    this.hero$ = this.service.getHero(heroId);
}
10.惰性加载

你可以配置路由定义来实现惰性加载模块,这意味着Angular只会在需要时才会加载这些模块,而不是在应用启动时就加载全部

11.路由守卫

Angular 中提供了以下路由守卫:

 export class YourGuard implements CanActivate {
  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): boolean {
      // your  logic goes here
  }
}

在路由模块中,在 routes 配置中使用相应的属性。这里的 canActivate 会告诉路由器它要协调到这个特定路由的导航

{
  path: '/your-path',
  component: YourComponent,
  canActivate: [YourGuard],
}
12.链接参数数组

链接参数数组保存路由导航时所需的成分:

  • 指向目标组件的那个路由的路径(path)
  • 必备路由参数和可选路由参数,它们将进入该路由的 URL
<a [routerLink]="['/heroes']">Heroes</a>

<a [routerLink]="['/hero', hero.id]">
  <span class="badge">{{ hero.id }}</span>{{ hero.name }}
</a>

<a [routerLink]="['/crisis-center', { foo: 'foo' }]">Crisis Center</a>
13.设置路由类型

路由器通过两种 LocationStrategy 提供者来支持所有这些风格:

  1. PathLocationStrategy - 默认的策略,支持“HTML 5 pushState”风格。
  2. HashLocationStrategy - 支持“hash URL”风格。

RouterModule.forRoot() 函数把 LocationStrategy 设置成了 PathLocationStrategy,使其成为了默认策略。 你还可以在启动过程中改写(override)它,来切换到 HashLocationStrategy 风格。

a. 路由器使用浏览器的histoery.pushState API 进行导航,借助pushState你自定义应用中的URL路径localhost:4200/crisis-center, 应用内的URL和服务器的URL没有区别。

<base href="/">

七、路由器参考使用

1.路由器导入

Angular的Router是一个可选组件,它为指定的URL提供特定的组件视图

import { RouterModule, Routes } from '@angular/router';

2.配置

带路由的Angular应用中一个Router服务的单例实例,当浏览器的URL发生变化时,该路由器会查找相应的router,以便根据它确定要显示的组件

const appRoutes: Routes = [
  { path: 'crisis-center', component: CrisisListComponent },
  { path: 'hero/:id',      component: HeroDetailComponent },
  {
    path: 'heroes',
    component: HeroListComponent,
    data: { title: 'Heroes List' }
  },
  { path: '',
    redirectTo: '/heroes',
    pathMatch: 'full'
  },
  { path: '**', component: PageNotFoundComponent }
];

@NgModule({
  imports: [
    RouterModule.forRoot(
      appRoutes,
      { enableTracing: true } // <-- debugging purposes only
    )
    // other imports here
  ],
  ...
})
export class AppModule { }

3.路由出口

<router-outlet></router-outlet>

4.路由链接

<nav>
  <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
  <a routerLink="/heroes" routerLinkActive="active">Heroes</a>
</nav>

5.活动路由链路

RouterLinkActive 指令会根据当前的RouterState切换活动RouterLink上锁绑定的css类.在每个a标签上,你会看到一个到RouterLinkActive指令的属性绑定,就像routerLinkActive = ‘…’

6.路由器状态

每个成功的导航生命周期结束后,路由器都会构建一个ActivatedRoute对象树,它构成了路由器的当前状态。你可以从任何地方使用应用的Router服务和routerState属性来访问当前的RouterState. RouterStata中的每个ActivedRoute都提供了向上或向下遍历路由树的方法,用于从父路由、子路由和兄弟路由中获取信息.

7.激活路由

路由的路径和参数可以通过注入名为ActivatedRoute的路由服务获得,他提供了大量有用的信息:

url       :   一个路由由路径的Observable,是一个由路由路径的各个部分组成的字符串数组
data      :  包含提供给当前路由的data对象的Observable,也包含任何路由解析守卫解析出来的值
paramMap  : 一个包含该路由的必要参数和可选参数map的Observable,这个mao支持从同一参数中获取得单个或多个值
queryParamMap:一个包含适用于所有路由的查询参数map的Observable.这个mao支持从同一个查询参数中获得单个或多个值。
fragment: 一个适用于所有路由的URL片段的Obserrvable
outlet: 用来渲染该路由的RouteOutlet的名字,对于无名出口,这个出口的名字是primary.
routeConfig: 包含原始路径的那个路由的配置信息。
parent: 当该路由是子路由时,表示该路由的父级ActivatedRoute
firstChild: 包含该路由的子路由列表中的第一个ActiveRoute.
children: 包含当前路由下所有激活的子路由。

8.路由器事件

NavitationStart        导航开始时触发的事件
RouteConfigLoadStart   在Router惰性加载路由配置之前触发的事件
RouteConfigLoadEnd     在某个路由已经惰性加载完毕时触发的事件
RoutesRecognized       当路由器解析了URL,而且路由已经识别完毕时触发的事件
GuardsCheckStart       当路由器开始进入路由守卫阶段是触发的事件
ChildActivationStart   当路由器开始激活某路由的子路由时触发的事件
ActivationStart        当路由器开始激活某个路由时触发的事件
GuardsCheckEnd         当路由器成功结束了路由守卫阶段是触发的事件
ResolveStart           当路由器开始解析解析阶段时触发的事件
ResolveEnd             当路由器的路由解析阶段成功完成时触发的事件
ChildActivationEnd     当路由成功激活某路由的子路由时触发的事件
ActivationEnd          当路由器成功停止时激活某个路由时触发的事件
NavigationEnd          当导航成功结束时触发的事件
NavigationCancel       当导航被取消时触发的事件,这可能在导航期间某个路由守卫返回了false或者返回了                            URLTree以进行重定向时发生
NavigationError        当导航由于非预期的错误而失败是触发的事件
Scroll                 用来表示滚动的事件

9.路由术语

Router       为活动URL显示应用的组件,管理从一个组件到另一个的导航
RouterModule 一个单独的NgModule,它提供了一些必要的服务提供者和一些用于在应用视图间导航的指令
Rourtes     定义一个路由数组,每一个条目都会把一个URL路径映射到组件
Route       定义路由器如何基于一个URL模式导航到某个组件,大部分路由器都由一个路径和一个组件组成
RouteOutlet  该指令<router-outlet>用于指出路由器应该把视图显示在哪里
RouterLink   用于将可点击的HTMl元素绑定到某个路由的指令,单击带有routerLink指令且绑定到字符串或链接参数              数组的元素,将触发导航
RouterLinkActive  该指令会在元素上或元素内包含的相关routerLink处于活动/非活动状态时,从HTML元素上添加/               移除类
ActivatedRoute 一个提供给每个路由组件的服务,其中包含当前路由专属的信息,
RouterState  路由器的当前状态,包括一颗当前激活路由的树以及遍历这棵树的便捷方法
链接参数数组   一个路由器将其解释为路由指南的数组,你可以将该数组绑定到RouterLink或将该数组作为参数传给                    Router.navigate方法
路由组件       一个带有RouterOutlet的Angulr组件,可基于路由器的导航来显示视图

八、HTTP客户端

Angular 给应用提供了一个 HTTP 客户端 API,也就是 @angular/common/http 中的 HttpClient 服务类。

HTTP 客户端服务提供了以下主要功能。

1.http请求
a.配置发起请求
// app/app/modules.ts
 
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  imports: [
    BrowserModule,
    // import HttpClientModule after BrowserModule.
    HttpClientModule,
  ],
  declarations: [
    AppComponent,
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule {}


//  ./config/config.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable()
export class ConfigService {
  constructor(private http: HttpClient) { }
  
  getConfig() {
  return this.http.get<Config>(this.configUrl);
  }
}


// config.components.ts

showConfig() {
  this.configService.getConfig()
    .subscribe((data: Config) => this.config = {
        heroesUrl: data.heroesUrl,
        textfile:  data.textfile,
        date: data.date,
    });
}
b.jsonp跨域

Angular 的 JSONP 请求会返回一个 Observable。 遵循订阅可观察对象变量的模式,并在使用 async 管道管理结果之前,使用 RxJS map 操作符转换响应。

/* GET heroes whose name contains search term */
searchHeroes(term: string): Observable {
  term = term.trim();

  const heroesURL = `${this.heroesURL}?${term}`;
  return this.http.jsonp(heroesUrl, 'callback').pipe(
      catchError(this.handleError('searchHeroes', [])) // then handle the error
    );
}
c.请求非JSON数据
getTextFile(filename: string) {
  return this.http.get(filename, {responseType: 'text'})
    .pipe(
      tap( 
        data => this.log(filename, data),
        error => this.logError(filename, error)
      )
    );
}

这里的 HttpClient.get() 返回字符串而不是默认的 JSON 对象,因为它的 responseType 选项是 'text'

2.处理请求错误
a. 获取错误详情
private handleError(error: HttpErrorResponse) {
  if (error.status === 0) {
    console.error('An error occurred:', error.error);
  } else {
    console.error(
      `Backend returned code ${error.status}, ` +
      `body was: ${error.error}`);
  }
  return throwError(
    'Something bad happened; please try again later.');
}

该处理程序会返回一个带有用户友好的错误信息的 RxJS ErrorObservable。下列代码修改了 getConfig() 方法,它使用一个管道HttpClient.get() 调用返回的所有 Observable 发送给错误处理器。

// config/config.service.ts
getConfig() {
  return this.http.get<Config>(this.configUrl)
    .pipe(
      catchError(this.handleError)
    );
}
3.重试失败的请求

RxJS 库提供了几个重试操作符。例如,retry() 操作符会自动重新订阅一个失败的 Observable 几次。重新订阅 HttpClient 方法会导致它重新发出 HTTP 请求。

getConfig() {
  return this.http.get<Config>(this.configUrl)
    .pipe(
      retry(3), 
      catchError(this.handleError) 
    );
}
4.把数据发送到服务器
a.post
addHero(hero: Hero): Observable<Hero> {
  return this.http.post<Hero>(this.heroesUrl, hero, httpOptions)
    .pipe(
      catchError(this.handleError('addHero', hero))
    );
}

HttpClioent.post() 方法像get()一样也有类型参数,可以用来指出你期望服务器返回特定类型的数据,该方法需要一个资源URL和两个额外的参数:

body: 要在请求体中post过去的数据

options: 一个包含方法选项的对象,在这里,它用来指定必要的请求头

b. DELETE
deleteHero(id: number): Observable<unknown> {
  const url = `${this.heroesUrl}/${id}`; // DELETE api/heroes/42
  return this.http.delete(url, httpOptions)
    .pipe(
      catchError(this.handleError('deleteHero'))
    );
}
c.put
updateHero(hero: Hero): Observable<Hero> {
  return this.http.put<Hero>(this.heroesUrl, hero, httpOptions)
    .pipe(
      catchError(this.handleError('updateHero', hero))
    );
}
5.添加和更新请求头
a.添加请求头
import { HttpHeaders } from '@angular/common/http';

const httpOptions = {
  headers: new HttpHeaders({
    'Content-Type':  'application/json',
    Authorization: 'my-auth-token'
  })
};
b.更新请求头
httpOptions.headers =
  httpOptions.headers.set('Authorization', 'my-new-auth-token');
6.配置HTTP URL参数
import {HttpParams} from "@angular/common/http";

searchHeroes(term: string): Observable<Hero[]> {
  term = term.trim();

  // Add safe, URL encoded search parameter if there is a search term
  const options = term ?
   { params: new HttpParams().set('name', term) } : {};

  return this.http.get<Hero[]>(this.heroesUrl, options)
    .pipe(
      catchError(this.handleError<Hero[]>('searchHeroes', []))
    );
}
7.拦截请求和响应
import { Injectable } from '@angular/core';
import {
  HttpEvent, HttpInterceptor, HttpHandler, HttpRequest
} from '@angular/common/http';

import { Observable } from 'rxjs';

/** Pass untouched request through to the next request handler. */
@Injectable()
export class NoopInterceptor implements HttpInterceptor {

  intercept(req: HttpRequest<any>, next: HttpHandler):
    Observable<HttpEvent<any>> {
    return next.handle(req);
  }
}

intercept 方法会把请求转换成一个最终返回 HTTP 响应体的 Observable。 在这个场景中,每个拦截器都完全能自己处理这个请求。

next 对象表示拦截器链表中的下一个拦截器,这个链表中的最后一个next对象就是HttpClient的后端处理器,它会把请求发给服务器,并接受服务器的响应。大多数的拦截器都会调用next.handle(),以便这个请求流能走到下一个拦截器,拦截器也可以不调用next.handle(),使这个链路短路,并返回一个带有人工构造出来的服务器响应的自己的Observable.

a.提供这个拦截器
import { HTTP_INTERCEPTORS } from '@angular/common/http';

import { NoopInterceptor } from './noop-interceptor';

/** Http interceptor providers in outside-in order */
export const httpInterceptorProviders = [
  { provide: HTTP_INTERCEPTORS, useClass: NoopInterceptor, multi: true },
];


//app/app.module.ts
providers: [
  httpInterceptorProviders
],

这个NoopInterceptor就是一个由Angular依赖注入系统管理的服务。像其他服务一样,你也必须要先提供这个拦截器,应用才能使用它。由于拦截器是HttpClient服务的依赖,所以你必须在提供HttpClient的同一个注入器中提供这些拦截器,那些在DI创建完HttpClient之后再提供的拦截器将会被忽略。

multi: true 这个必须的选择会告诉Angular Http_INTERCEPTORS是一个多重提供者的令牌,表示它会注入一个多值的数组,而不是单一的值

b.处理拦截器事件

​ 虽然拦截器有能力改变请求和响应,但 HttpRequestHttpResponse 实例的属性却是只读(readonly)的, 因此让它们基本上是不可变的。如果你必须修改一个请求,先把它克隆一份,修改这个克隆体后再把它传给 next.handle()。你可以在一步中克隆并修改此请求,

const secureReq = req.clone({
  url: req.url.replace('http://', 'https://')
});
// send the cloned, "secure" request to the next handler.
return next.handle(secureReq);
c.修改请求体

如果必须修改请求体,请执行以下步骤。

  1. 复制请求体并在副本中进行修改。
  2. 使用 clone() 方法克隆这个请求对象。
  3. 用修改过的副本替换被克隆的请求体。
const newBody = { ...body, name: body.name.trim() };
const newReq = req.clone({ body: newBody });
return next.handle(newReq);
d.拦截器用例
  1. 设置默认请求头
import { AuthService } from '../auth.service';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor(private auth: AuthService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler) {
    // Get the auth token from the service.
    const authToken = this.auth.getAuthorizationToken();

    // Clone the request and replace the original headers with
    // cloned headers, updated with the authorization.
    const authReq = req.clone({
      headers: req.headers.set('Authorization', authToken)
    });

    // send cloned request with header to the next handler.
    return next.handle(authReq);
  }
}

2.记录请求与相应对

​ 因为拦截器可以同事处理请求和响应,所以它们也可以对整个HTTP操作记录执行计时和记录日志等任务.考虑下面这个LoggingInterceprot,它铺货请求的发起时间、响应的接受时间,并使用注入的MEssageService来发送总共花费的时间。

import { finalize, tap } from 'rxjs/operators';
import { MessageService } from '../message.service';

@Injectable()
export class LoggingInterceptor implements HttpInterceptor {
  constructor(private messenger: MessageService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler) {
    const started = Date.now();
    let ok: string;
    return next.handle(req)
      .pipe(
        tap(
          event => ok = event instanceof HttpResponse ? 'succeeded' : '',
          error => ok = 'failed'
        ),
        finalize(() => {
          const elapsed = Date.now() - started;
          const msg = `${req.method} "${req.urlWithParams}"
             ${ok} in ${elapsed} ms.`;
          this.messenger.add(msg);
        })
      );
  }
}

Rxjs的tap操作符会捕获请求成功了还是失败了,Rxjs的finalize操作符无论是在响应成功还是失败时都会调用,然后把结果汇报给MessageService.在这个可观察对象的流中,无论是tap还是finslize接触过的值,都会照常发给调用者。

3.自定义json解析

拦截器可用来实现自定义替换内置的json解析。通过注入的JsonParser来实现json解析

@Injectable()
export abstract class JsonParser {
  abstract parse(text: string): any;
}

@Injectable()
export class CustomJsonInterceptor implements HttpInterceptor {
  constructor(private jsonParser: JsonParser) {}

  intercept(httpRequest: HttpRequest<any>, next: HttpHandler) {
    if (httpRequest.responseType === 'json') {
      return this.handleJsonResponse(httpRequest, next);
    } else {
      return next.handle(httpRequest);
    }
  }

  private handleJsonResponse(httpRequest: HttpRequest<any>, next: HttpHandler) {
    httpRequest = httpRequest.clone({responseType: 'text'})
    return next.handle(httpRequest).pipe(map(event => this.parseJsonResponse(event)));
  }

  private parseJsonResponse(event: HttpEvent<any>) {
    if (event instanceof HttpResponse && typeof event.body === 'string') {
      return event.clone({body: this.jsonParser.parse(event.body)});
    } else {
      return event;
    }
  }
}

自定义实现JsonParser

@Injectable()
export class CustomJsonParser implements JsonParser {
  parse(text: string): any {
    return JSON.parse(text, dateReviver);
  }
}

4.用拦截器实现缓存

拦截器还可以自行处理这些请求,而不用转发给next.handle()

@Injectable()
export class CachingInterceptor implements HttpInterceptor {
  constructor(private cache: RequestCache) {}

  intercept(req: HttpRequest<any>, next: HttpHandler) {
    // continue if not cacheable.
    if (!isCacheable(req)) { return next.handle(req); }

    const cachedResponse = this.cache.get(req);
    return cachedResponse ?
      of(cachedResponse) : sendRequest(req, next, this.cache);
  }
}

(1)isChcheable()函数用于决定该请求是否允许缓存,

(2)如何该函数是不可能缓存的,该拦截器会把该请求转发给链表中的下一个处理器

​ (3) 如果可缓存的请求不在缓存中,代码会调用sendREquest().这个函数会创建一个没有请求头的请求克隆体然后,该函数把请求的克隆体转发给next.handle(),它会最终调用服务器返回来自服务器的响应对象。

(4)如果可缓存的请求在缓存中找到了,该拦截器就会通过of()函数返回一个已缓存的响应体的可观察对象,然后绕过next处理器

function sendRequest(
  req: HttpRequest<any>,
  next: HttpHandler,
  cache: RequestCache): Observable<HttpEvent<any>> {

  // No headers allowed in npm search request
  const noHeaderReq = req.clone({ headers: new HttpHeaders() });

  return next.handle(noHeaderReq).pipe(
    tap(event => {
      // There may be other events besides the response.
      if (event instanceof HttpResponse) {
        cache.put(req, event); // Update the cache.
      }
    })
  );
}

注意sendRequest()是如何在返回应用程序的过程中拦截响应的,该方法通过tap()操作符来管理响应对象,该操作符的回调函数就会把响应对象添加到缓存中。然后,原始的响应就会通过这些拦截器链,原封不对的回到服务器的调用者那里。

5.用拦截器来请求多个值

HttpClient.get() 方法通常会返回一个可观察对象,它会发出一个值,拦截器可以把它改成一个可以发出多个值的可观察对象。修改后的CachingInterceptor可以返回一个立即返回发出所缓存响应的可观察对象,然后把请求发送到NPM的Web API, 然后把修改过得搜索结果重新发出一次。

// cache-then-refresh
if (req.headers.get('x-refresh')) {
  const results$ = sendRequest(req, next, this.cache);
  return cachedResponse ?
    results$.pipe( startWith(cachedResponse) ) :
    results$;
}
// cache-or-fetch
return cachedResponse ?
  of(cachedResponse) : sendRequest(req, next, this.cache);

cache-then-refresh 选项是一个自定义的x-refresh请求头触发的

PackageSearchComponent中的一个检查框会切换withRefresh标识,它是PackageSearchService.search()的参数之一,search()方法创建了自定义的x-refresh头,并在调用HttpCLient.get()前把它添加在请求里。

6.跟踪和显示请求进度

​ 应用程序有时会传输大量数据,而这些传输可能要花很长时间,你可以通过此类传输的进度反馈,为用户提供更好的体验。

​ 要想发出一个带有进度事件的请求,你可以创建一个HttpRquest实例,并把resportProgress选项设置为treu来秦东对进度事件的跟踪。

const req = new HttpRequest('POST', '/upload/file', file, {
  reportProgress: true
});

该方法返回一个HttpEvents的Observale

return this.http.request(req).pipe(
  map(event => this.getEventMessage(event, file)),
  tap(message => this.showProgress(message)),
  last(), 
  catchError(this.handleError(file))
);

getEventMessage 方法解释了事件流中每种类型的HttpEvent

private getEventMessage(event: HttpEvent<any>, file: File) {
  switch (event.type) {
    case HttpEventType.Sent:
      return `Uploading file "${file.name}" of size ${file.size}.`;

    case HttpEventType.UploadProgress:
      // Compute and show the % done:
      const percentDone = Math.round(100 * event.loaded / (event.total ?? 0));
      return `File "${file.name}" is ${percentDone}% uploaded.`;

    case HttpEventType.Response:
      return `File "${file.name}" was completely uploaded!`;

    default:
      return `File "${file.name}" surprising upload event: ${event.type}.`;
  }
}
6.通过防抖来优化与服务器的交互
this.packages$ = this.searchText$.pipe(
    debounceTime(500),
    distinctUntilChanged(),
    switchMap(packageName =>
      this.searchService.search(packageName, this.withRefresh))
  );

ngOnInit() 中的代码还通过下列三个操作符对这些搜索值进行管道处理,以便只有当它是一个新值并且用户已经停止输入时,要搜索的值才会抵达该服务。

  • debounceTime(500)⁠—等待用户停止输入(本例中为 1/2 秒)。

  • distinctUntilChanged()⁠—等待搜索文本发生变化。

    distinctUntilChanged()⁠—Wait until the search text changes.

  • switchMap()⁠—将搜索请求发送到服务。

这些代码把 packages$ 设置成了使用搜索结果组合出的 Observable 对象。 模板中使用 AsyncPipe 订阅了 packages$,一旦搜索结果的值发回来了,就显示这些搜索结果。

7.安全:XSRF防护

跨站请求伪造(XSRF / CSRF )是一个攻击技术,他能让攻击者假冒一个已认证的用户在你的网站上执行未知的操作。HttpClient支持一种通用的机制来防范XSRF攻击,当执行HTTP请求时,一个拦截器会从cookie中读取XSRF令牌,并且把他设置为一个HTTP请求头X_XSRF_TOKEN,由于只有运行在你的域名下的代码才能读取这个cookie因此后端可以确认这个http请求真的来自你的客户端应用,而不是攻击者

配置自定义cookie/header名称

如果你的后端服务中对XSRF令牌的cookie或者头使用了不一样的名字,就需要使用HttpClientXsrfModule.withConfig() 来覆盖掉默认值

imports: [
  HttpClientModule,
  HttpClientXsrfModule.withOptions({
    cookieName: 'My-Xsrf-Cookie',
    headerName: 'My-Xsrf-Header',
  }),
],

8.测试http请求

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值