Angular: работа с сервером, HttpClient

Пример запросов на сервер

Хорошой практикой является вынесение обоаботки самих запросов в сервис. Хотя, если бы приложение было однокомпонентное, то нормально было бы прописывать их и в родительском компоненте.

Пример файла сервиса, где выполняются запросы:

import {Injectable} from '@angular/core'
import {HttpClient} from '@angular/common/http'
import {Observable} from 'rxjs'
import {delay} from 'rxjs/operators'

export interface Todo {
  completed: boolean
  title: string
  id?: number
}

@Injectable({providedIn: 'root'})
export class TodosService {
  constructor(private http: HttpClient) {}

  addTodo(todo: Todo): Observable<Todo> {
    return this.http.post<Todo>('https://jsonplaceholder.typicode.com/todos', todo)
  }

  fetchTodos(): Observable<Todo[]> {
    return this.http.get<Todo[]>('https://jsonplaceholder.typicode.com/todos?_limit=2')
      .pipe(delay(500))
  }

  removeTodo(id: number): Observable<void> {
    return this.http.delete<void>(`https://jsonplaceholder.typicode.com/todos/${id}`)
  }

  completeTodo(id: number): Observable<Todo> {
    return this.http.put<Todo>(`https://jsonplaceholder.typicode.com/todos/${id}`, {
      completed: true
    })
  }
}

Файл компонента, из который обращается для выполнения этих запросов:

import {Component, OnInit} from '@angular/core'
import {Todo, TodosService} from './todos.service'

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {

  todos: Todo[] = []
  loading = false
  todoTitle = ''

  constructor(private todosService: TodosService) {}

  ngOnInit() {
    this.fetchTodos()
  }

  addTodo() {
    if (!this.todoTitle.trim()) {
      return
    }

    this.todosService.addTodo({
      title: this.todoTitle,
      completed: false
    }).subscribe(todo => {
      this.todos.push(todo)
      this.todoTitle = ''
    })
  }

  fetchTodos() {
    this.loading = true
    this.todosService.fetchTodos()
      .subscribe(todos => {
        this.todos = todos
        this.loading = false
      })
  }

  removeTodo(id: number) {
    this.todosService.removeTodo(id)
      .subscribe(() => {
        this.todos = this.todos.filter(t => t.id !== id)
      })
  }

  completeTodo(id: number) {
    this.todosService.completeTodo(id).subscribe(todo => {
      this.todos.find(t => t.id === todo.id).completed = true
    })
  }
}

Обработка ошибок

Путей отлавливания ошибок существует много, но вот 2 самых простых:

1) Ловим ошибку в файле компонента при помощи встроенного метода в HttpClient

error = ''

fetchTodos() {
   this.loading = true
   this.todosService.fetchTodos()
     .subscribe(todos => {
       this.todos = todos
       this.loading = false
     }, error => {
       this.error = error.message
     })
 }

2) Ловим ошибку в файле сервиса: при помощи встроеных инструментов в RxJS

Например, учитывая что в нас все стремы, мы обрабатываем метод pipe (этот метод может принимать разные параметры, в т.ч. delay(500) в примере, которые создает задержку выполнения, а также map(), tap() и т.д. для обработки ответа).

import {Observable, throwError} from 'rxjs'
...
fetchTodos(): Observable<Todo[]> {
  return this.http.get<Todo[]>('https://jsonplaceholder.typicode.com/todosf?_limit=2')
    .pipe(
      delay(500),
      catchError(error => {
        console.log('Error: ', error.message)
        return throwError(error)
      })
    )
}

Передача body и headers

Документация

Body (тело запроса) передается просто в виде объекта вторым параметром. А вот чтобы передать хедеры, то нужно добавить их параметром после тела запроса, используя для создания специальный класс HttpHeaders:

cosnt headers = new HttpHeaders({"ByCustomHeader: 'some data'"})
addTodo(todo: Todo): Observable<Todo> { 
   return this.http.post<Todo>('https://jsonplaceholder.typicode.com/todos', todo, headers) 
}

Либо можно еще проще – прямо этот новый класс класть в параметр, а не переменную с ним.

Передача query параметров

По аналогии с хедерами можно передавать параметры запроса. В get-запросе опциональный (содержит объект с опциями) второй параметр, так как не передается body, то в него и можно положить query параметры.

1) Простой пример, где один параметр:

fetchTodos(): Observable<Todo[]> {
   return this.http.get<Todo[]>('https://jsonplaceholder.typicode.com/todos',
       params: new HttpParams().set('_limit', '3')})
     .pipe(
         catchError(error => {
         console.log('Error: ', error.message)
         return throwError(error)
       })
     )
 }

2) Более сложный пример, когда больше одного параметра надо передать:

fetchTodos(): Observable<Todo[]> {
   let params = new HttpParams()
   params = params.append('_limit', '4');
   params = params.append('custom', 'anything')
   return this.http.get<Todo[]>('https://jsonplaceholder.typicode.com/todos', params})
     .pipe(
         catchError(error => {
         console.log('Error: ', error.message)
         return throwError(error)
       })
     )
 }

Другие параметры в options, которые могут пригодится

Еще к опциональным параметрам, которые возможно когда-то понядобятся, относятся observe и responseType.

observe указывает, что мы принимаем в качестве ответа. По умолчанию в нем стоит body, но его можно имзенеить на response (более полный объект, в котором лежит тот же body) или на events, благодаря которому можно отслеживать все этапы обработки запроса.

responseType указывает, какой тип данных принимать, так как по умолчанию Angular сам обрабатывает данные при помощи JSON.parse. Но можно, например, указать responseType: text, и будет приходить строка, которую придется обрабатывать вручную.

Интерсепторы для обработки запросов

Интерсепторы – это некие классы, которые позволяют перехватывать http запросы и что-то с ними делать.

1) Создаем файл с интерсептором

Для их создания создается новый файл в папке с компонентом, например auth.interceptor.ts. Называть его можно по-разному, например через дефис (auth-inerceptor.service.ts), но так название более длинное получается. По факту interceptor является своего рода сервисом.

При объявлении класса внутри интерсептора надо имплементироваться от специального встроеного интерфейса HttpInterceptor:

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

export class AuthInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    console.log('Intercept request', req)
    return next.handle(req)
  }
}

2) Импортируем в файл модулей

Импортируется интерсептор в providers, но немного иным образом – используя константу HTTP_INTERCEPTORS.

import {BrowserModule} from '@angular/platform-browser'
import {NgModule, Provider} from '@angular/core'

import {AppComponent} from './app.component'
import {FormsModule} from '@angular/forms';
import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http'
import {AuthInterceptor} from './auth.interceptor' // <=== здесь

const INTERCEPTOR_PROVIDER: Provider = { // <=== здесь
  provide: HTTP_INTERCEPTORS,
  useClass: AuthInterceptor,
  multi: true
}

@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpClientModule
  ],
  providers: [INTERCEPTOR_PROVIDER], // <=== здесь
  bootstrap: [AppComponent]
})
export class AppModule {
}

3) Обрабатываем запрос

В примере выше мы просто возвращаем тот же запрос, что и был, т.е. не изменяем его. Допустим нам надо прикрепить ко всем запросам в заголовке токен, тогда сначала при помощи метода req.clone создаем копию запроса (оригинал менять нельзя) и прикрепляем к нему нужный заголовок:

import {HttpEvent, HttpEventType, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http'
import {Observable} from 'rxjs'
import {tap} from 'rxjs/operators'

export class AuthInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    console.log('Intercept request', req)

    const cloned = req.clone({
      headers: req.headers.append('Auth', 'SOME RANDOM TOKEN')
    })
    return next.handle
  }
}

Кроме того, возвращаемое значение является стримом. И у любого стрима мы можем вызывать его методы, например, pipe для добавления нового функционала. В т.ч. отслеживать event:

...
    return next.handle(cloned).pipe(
      tap(event => {
        if (event.type === HttpEventType.Response) {
          console.log('Interceptor response', event)
        }
      })
    )
...

 

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *