MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Angular指令与HTTP请求:动态数据加载

2023-03-106.9k 阅读

Angular指令基础

在Angular中,指令是一种扩展HTML的机制,它允许我们为DOM元素添加行为。Angular中有三种类型的指令:组件(Component)、结构型指令(Structural Directive)和属性型指令(Attribute Directive)。

组件指令

组件是一种特殊的指令,它有自己的模板、样式和逻辑。每个组件都是一个独立的可复用单元。例如,我们创建一个简单的HelloComponent

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

@Component({
  selector: 'app-hello',
  templateUrl: './hello.component.html',
  styleUrls: ['./hello.component.css']
})
export class HelloComponent {
  message = 'Hello, Angular!';
}

hello.component.html模板中:

<p>{{message}}</p>

在其他组件的模板中,我们可以通过<app-hello></app-hello>来使用这个组件。

结构型指令

结构型指令用于改变DOM的结构。常见的结构型指令有*ngIf*ngFor等。

*ngIf根据表达式的值来决定是否渲染一个元素。例如:

<div *ngIf="isLoggedIn">
  <p>Welcome, user!</p>
</div>

在组件类中:

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

@Component({
  selector: 'app-example',
  templateUrl: './example.component.html'
})
export class ExampleComponent {
  isLoggedIn = true;
}

这里,当isLoggedIntrue时,<div>及其内部内容会被渲染到DOM中。

*ngFor用于遍历一个集合,并为集合中的每个元素创建一个模板实例。例如,我们有一个数组:

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

@Component({
  selector: 'app-list',
  templateUrl: './list.component.html'
})
export class ListComponent {
  items = ['Apple', 'Banana', 'Cherry'];
}

list.component.html中:

<ul>
  <li *ngFor="let item of items">{{item}}</li>
</ul>

这会在DOM中创建一个无序列表,每个列表项对应items数组中的一个元素。

属性型指令

属性型指令用于改变元素的外观或行为。例如,ngStyle指令可以根据表达式动态地设置元素的样式。

<div [ngStyle]="{'color': isHighlighted? 'red' : 'black'}">
  This text color changes based on isHighlighted value.
</div>

在组件类中:

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

@Component({
  selector: 'app-style',
  templateUrl: './style.component.html'
})
export class StyleComponent {
  isHighlighted = true;
}

这里,根据isHighlighted的值,文本的颜色会在红色和黑色之间切换。

创建自定义指令

除了使用Angular提供的内置指令,我们还可以创建自己的指令。自定义指令可以帮助我们封装特定的行为,提高代码的复用性。

创建属性型自定义指令

假设我们要创建一个指令,当鼠标悬停在元素上时,改变元素的背景颜色。首先,使用Angular CLI生成一个指令:

ng generate directive highlight

这会生成一个highlight.directive.ts文件:

import { Directive, ElementRef, HostListener } from '@angular/core';

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective {
  constructor(private el: ElementRef) {}

  @HostListener('mouseenter') onMouseEnter() {
    this.highlight('yellow');
  }

  @HostListener('mouseleave') onMouseLeave() {
    this.highlight(null);
  }

  private highlight(color: string) {
    this.el.nativeElement.style.backgroundColor = color;
  }
}

在模板中使用这个指令:

<p appHighlight>Hover over me to see the highlight effect.</p>

这里,@Directive装饰器定义了指令的选择器[appHighlight]@HostListener装饰器用于监听宿主元素(即应用了该指令的元素)的事件,mouseentermouseleave事件分别触发onMouseEnteronMouseLeave方法,进而调用highlight方法来改变背景颜色。

创建结构型自定义指令

结构型自定义指令相对复杂一些。我们以创建一个类似*ngIf的指令*appUnless为例,它的作用是当表达式为false时渲染元素。

首先生成指令:

ng generate directive unless

unless.directive.ts中:

import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[appUnless]'
})
export class UnlessDirective {
  private hasView = false;

  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef
  ) {}

  @Input() set appUnless(condition: boolean) {
    if (!condition &&!this.hasView) {
      this.viewContainer.createEmbeddedView(this.templateRef);
      this.hasView = true;
    } else if (condition && this.hasView) {
      this.viewContainer.clear();
      this.hasView = false;
    }
  }
}

在模板中使用:

<div *appUnless="isLoggedIn">
  <p>Please log in.</p>
</div>

这里,@Input装饰器用于接收外部传入的条件值。TemplateRef代表模板引用,ViewContainerRef用于管理视图。当条件为false且当前没有视图时,创建视图;当条件为true且当前有视图时,清除视图。

Angular中的HTTP请求

在前端开发中,与后端服务器进行数据交互是非常常见的需求。Angular提供了强大的HttpClient模块来处理HTTP请求。

配置HTTP模块

首先,在app.module.ts中导入HttpClientModule

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform - browser';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, HttpClientModule],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

这样就可以在应用中使用HttpClient了。

发送GET请求

假设我们有一个后端API,地址为https://example.com/api/users,返回用户列表。我们在组件中发送GET请求获取数据:

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app - user - list',
  templateUrl: './user - list.component.html'
})
export class UserListComponent {
  users: any[] = [];

  constructor(private http: HttpClient) {
    this.http.get<any[]>('https://example.com/api/users').subscribe(data => {
      this.users = data;
    });
  }
}

user - list.component.html中:

<ul>
  <li *ngFor="let user of users">{{user.name}}</li>
</ul>

这里,http.get方法发送一个GET请求到指定的URL,subscribe方法用于处理响应数据,将返回的用户列表赋值给users数组,然后在模板中通过*ngFor指令展示用户列表。

发送POST请求

当我们需要向服务器提交数据时,可以使用POST请求。例如,我们有一个创建用户的API https://example.com/api/users,请求体包含用户信息。

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app - create - user',
  templateUrl: './create - user.component.html'
})
export class CreateUserComponent {
  newUser = { name: '', age: 0 };

  constructor(private http: HttpClient) {}

  createUser() {
    this.http.post('https://example.com/api/users', this.newUser).subscribe(response => {
      console.log('User created successfully:', response);
    });
  }
}

create - user.component.html中:

<form>
  <label>Name:
    <input type="text" [(ngModel)]="newUser.name">
  </label>
  <label>Age:
    <input type="number" [(ngModel)]="newUser.age">
  </label>
  <button type="button" (click)="createUser()">Create User</button>
</form>

这里,http.post方法的第一个参数是URL,第二个参数是请求体数据。当用户点击“Create User”按钮时,createUser方法被调用,向服务器发送POST请求创建新用户。

处理HTTP响应和错误

在实际应用中,我们需要处理HTTP请求的响应和可能出现的错误。subscribe方法可以接收三个回调函数,分别用于处理成功响应、错误和完成事件。

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app - data - fetch',
  templateUrl: './data - fetch.component.html'
})
export class DataFetchComponent {
  data: any;

  constructor(private http: HttpClient) {
    this.http.get('https://example.com/api/data').subscribe(
      response => {
        this.data = response;
        console.log('Data fetched successfully:', response);
      },
      error => {
        console.error('Error fetching data:', error);
      },
      () => {
        console.log('HTTP request completed.');
      }
    );
  }
}

在上述代码中,当请求成功时,response回调函数将响应数据赋值给data变量,并在控制台打印成功信息;当请求出错时,error回调函数在控制台打印错误信息;当请求完成时(无论成功或失败),complete回调函数在控制台打印完成信息。

使用指令实现动态数据加载

结合Angular指令和HTTP请求,我们可以实现动态数据加载的功能。例如,我们创建一个指令,当元素进入视口时,触发HTTP请求加载数据。

创建视口指令

首先,我们创建一个自定义结构型指令,用于检测元素是否进入视口。使用IntersectionObserver API来实现这一功能。

ng generate directive inViewport

in - viewport.directive.ts中:

import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[appInViewport]'
})
export class InViewportDirective {
  private observer: IntersectionObserver;
  private hasView = false;

  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef
  ) {
    this.observer = new IntersectionObserver((entries, observer) => {
      entries.forEach(entry => {
        if (entry.isIntersecting &&!this.hasView) {
          this.viewContainer.createEmbeddedView(this.templateRef);
          this.hasView = true;
          observer.unobserve(this.viewContainer.element.nativeElement);
        }
      });
    });
  }

  @Input() set appInViewport(element: HTMLElement) {
    if (element) {
      this.observer.observe(element);
    }
  }
}

这里,IntersectionObserver用于异步观察目标元素与其祖先元素或视口的交集变化情况。当元素进入视口且当前没有视图时,创建视图并停止观察。

结合HTTP请求实现动态加载

假设我们有一个图片列表,当图片进入视口时,通过HTTP请求加载图片的详细信息。

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app - dynamic - load',
  templateUrl: './dynamic - load.component.html'
})
export class DynamicLoadComponent {
  images = [
    { id: 1, url: 'image1.jpg' },
    { id: 2, url: 'image2.jpg' },
    { id: 3, url: 'image3.jpg' }
  ];
  imageDetails: { [key: number]: any } = {};

  constructor(private http: HttpClient) {}

  loadImageDetails(imageId: number) {
    if (!this.imageDetails[imageId]) {
      this.http.get(`https://example.com/api/images/${imageId}`).subscribe(data => {
        this.imageDetails[imageId] = data;
      });
    }
  }
}

dynamic - load.component.html中:

<div *ngFor="let image of images">
  <img [src]="image.url" alt="{{image.id}}">
  <div *appInViewport="elementRef">
    <button (click)="loadImageDetails(image.id)">Load Details</button>
    <div *ngIf="imageDetails[image.id]">
      <p>Details: {{imageDetails[image.id].description}}</p>
    </div>
  </div>
  <div #elementRef></div>
</div>

这里,*appInViewport指令检测包含“Load Details”按钮和图片详细信息区域的div是否进入视口。当进入视口时,显示按钮。点击按钮触发loadImageDetails方法,通过HTTP请求加载图片详细信息,并在模板中显示。

优化动态数据加载

在实现动态数据加载后,我们还可以进行一些优化,以提高应用的性能和用户体验。

缓存数据

为了避免重复请求相同的数据,我们可以在组件中实现简单的数据缓存机制。例如,在上述图片详细信息加载的例子中,我们已经通过imageDetails对象进行了一定程度的缓存。但我们可以进一步优化,比如在HTTP请求拦截器中实现更通用的缓存。

创建一个HTTP拦截器:

ng generate interceptor cacheInterceptor

cache - interceptor.ts中:

import { Injectable } from '@angular/core';
import {
  HttpEvent,
  HttpHandler,
  HttpInterceptor,
  HttpRequest
} from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class CacheInterceptor implements HttpInterceptor {
  private cache: { [url: string]: any } = {};

  intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    if (request.method === 'GET' && this.cache[request.url]) {
      return of(this.cache[request.url]);
    }

    return next.handle(request).pipe(
      tap((event: HttpEvent<any>) => {
        if (event instanceof HttpResponse && request.method === 'GET') {
          this.cache[request.url] = event.body;
        }
      })
    );
  }
}

然后在app.module.ts中注册拦截器:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform - browser';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { AppComponent } from './app.component';
import { CacheInterceptor } from './cache - interceptor';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, HttpClientModule],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: CacheInterceptor,
      multi: true
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

这样,对于相同的GET请求,拦截器会先检查缓存中是否有数据,如果有则直接返回缓存数据,避免重复请求。

处理并发请求

在实际应用中,可能会同时发起多个HTTP请求。为了避免过多的并发请求影响性能,我们可以限制并发请求的数量。

使用RxJSforkJoinconcatMap操作符来实现。假设我们有一个图片列表,需要同时加载多个图片的详细信息,但限制并发请求数量为2。

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { forkJoin, from, Observable } from 'rxjs';
import { concatMap } from 'rxjs/operators';

@Component({
  selector: 'app - concurrent - load',
  templateUrl: './concurrent - load.component.html'
})
export class ConcurrentLoadComponent {
  images = [
    { id: 1, url: 'image1.jpg' },
    { id: 2, url: 'image2.jpg' },
    { id: 3, url: 'image3.jpg' },
    { id: 4, url: 'image4.jpg' }
  ];
  imageDetails: { [key: number]: any } = {};

  constructor(private http: HttpClient) {
    const imageIds = this.images.map(image => image.id);
    const requests$ = from(imageIds).pipe(
      concatMap(id => this.http.get(`https://example.com/api/images/${id}`), (id, response) => ({ id, response })),
      (source) => {
        const chunks = [];
        let currentChunk = [];
        let count = 0;
        source.subscribe(({ id, response }) => {
          currentChunk.push({ id, response });
          count++;
          if (count % 2 === 0 || source.closed) {
            chunks.push(currentChunk);
            currentChunk = [];
          }
        });
        return forkJoin(chunks.map(chunk => forkJoin(chunk.map(({ id, response }) => {
          this.imageDetails[id] = response;
          return of(null);
        }))));
      }
    );
    requests$.subscribe();
  }
}

在上述代码中,from将图片ID数组转换为一个可观察对象,concatMap依次发送HTTP请求。通过自定义逻辑,将请求分成每组最多2个的块,使用forkJoin并行处理每个块中的请求,从而限制了并发请求数量。

安全性考虑

在进行HTTP请求时,安全性是至关重要的。

防止XSS攻击

跨站脚本攻击(XSS)是一种常见的Web安全漏洞。在Angular中,通过使用模板语法和数据绑定,Angular会自动对插入到DOM中的数据进行转义,从而防止大部分XSS攻击。例如:

<p>{{userInput}}</p>

如果userInput包含恶意脚本<script>alert('XSS')</script>,Angular会将其转义为文本显示,而不会执行脚本。

防止CSRF攻击

跨站请求伪造(CSRF)攻击是攻击者利用用户的登录状态,在用户不知情的情况下发送恶意请求。为了防止CSRF攻击,通常后端会生成一个CSRF令牌,前端在每次请求时带上这个令牌。

在Angular中,可以通过HTTP拦截器在请求头中添加CSRF令牌。假设后端在Cookie中设置了CSRF令牌,我们可以这样获取并添加到请求头:

ng generate interceptor csrfInterceptor

csrf - interceptor.ts中:

import { Injectable } from '@angular/core';
import {
  HttpEvent,
  HttpHandler,
  HttpInterceptor,
  HttpRequest
} from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class CsrfInterceptor implements HttpInterceptor {
  intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    const csrfToken = document.cookie.match(/XSRF - TOKEN=([^;]+)/);
    if (csrfToken) {
      request = request.clone({
        setHeaders: {
          'X - XSRF - Token': csrfToken[1]
        }
      });
    }
    return next.handle(request);
  }
}

然后在app.module.ts中注册拦截器:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform - browser';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { AppComponent } from './app.component';
import { CsrfInterceptor } from './csrf - interceptor';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, HttpClientModule],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: CsrfInterceptor,
      multi: true
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

这样,每次HTTP请求都会带上CSRF令牌,后端可以验证令牌的有效性,从而防止CSRF攻击。

通过深入理解Angular指令和HTTP请求,并合理运用它们来实现动态数据加载,同时注重性能优化和安全性,我们可以构建出高效、安全且用户体验良好的前端应用。在实际开发中,还需要根据具体的业务需求和场景,灵活运用这些知识,不断完善和优化应用。