Выполнение HTTP-запросов является жизненно важной операцией в жизни большинства интерфейсных приложений. Angular 2, который сейчас является самой популярной вещью, имеет действительно классный способ сделать это. На самом деле это то, что мы собираемся обсудить сегодня в этом уроке. Мы научимся делать HTTP-запросы с помощью библиотеки RxJs Observable.

Мы создадим приложение для комментариев. Вот демонстрация и краткий обзор:

И пара скриншотов для финального приложения:

Что такое наблюдаемые?

Наблюдаемые объекты похожи на обещания, но имеют существенные отличия, которые делают их лучше.

Observables — это новый примитив, появившийся в ES7 (ES2016), который помогает обрабатывать асинхронные действия и события. Observables похожи на promises, но имеют существенные отличия, которые делают их лучше. Ключевые отличия:

ObservablesPromiseObservables обрабатывают несколько значений с течением времени. Promises вызываются только один раз и возвращают одно значение. Observables можно отменить. Promises нельзя отменить.

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

Возможность отменить наблюдаемые объекты обеспечивает лучший контроль при работе с входящим потоком значений из потока. Типичным примером является виджет автозаполнения, который отправляет запрос на каждое нажатие клавиши.

Если вы ищете angular в автозаполнении, первый запрос будет с a, а затем с an. Страшно то, что может вернуться с ответом раньше, чем a, который выдает беспорядочные данные. С наблюдаемыми у вас есть лучший контроль, чтобы подключиться и отменить, потому что проходит.

Observables — это функция ES7, что означает, что вам нужно использовать внешнюю библиотеку, чтобы использовать ее сегодня. RxJS — хороший вариант. RxJS также предоставляет операторы Observable, которые вы можете использовать для управления испускаемыми данными. Некоторые из этих операторов:

  • карта
  • Фильтр
  • Брать
  • Пропускать
  • Отказаться

Выше приведен список популярных операторов, с которыми вы столкнетесь в большинстве проектов, но это не все. См. RxMarbles для получения дополнительной информации.

Angular 2 HTTP и наблюдаемые объекты

Надеюсь, вы видели, на что способны наблюдаемые объекты. Хорошая новость заключается в том, что вы также можете использовать наблюдаемые объекты для обработки HTTP-запросов, а не промисов. Я понимаю, что вы, возможно, начали в те дни, когда обратные вызовы были горячей вещью при обработке XHR, а затем пару лет назад вы узнали, что обратные вызовы теперь являются плохой практикой, и вам приходилось использовать промисы. И снова мы слышим, что нам следует использовать наблюдаемые данные, а не промисы.

Нам просто нужно привыкнуть к изменениям и росту, чтобы создавать лучшие и крутые вещи

Angular и Angular 2 потрясающие, теперь вы слышите, что вы должны использовать наблюдаемые объекты, а не промисы. Это общая техническая проблема, и нам просто нужно привыкнуть к изменениям и росту, чтобы создавать лучшие и крутые вещи. Поверьте, вы не пожалеете об этом.

Оставшаяся часть этой статьи будет посвящена созданию демо-версии, использующей наблюдаемые объекты для обработки HTTP-запросов.

Предпосылки

Angular Quickstart — хороший шаблон для базового проекта Angular, и нас должно это устроить. Клонируйте репозиторий и установите все его зависимости:

# Clone repo
git clone https://github.com/angular/quickstart scotch-http
# Enter into directory
cd scotch-http
# Install dependencies
npm install

Это дает хорошую платформу, чтобы запачкать руки.

В предоставленном демонстрационном репозитории есть папка сервера, которая обслуживает конечные точки API для нашего приложения. Создание этих конечных точек API выходит за рамки этой задачи, но это базовое приложение Node, созданное с помощью ES6, но транспилированное с помощью Babel. Когда вы клонируете демо, запустите следующее, чтобы запустить сервер:

# Move in to server project folder
cd server
# Install dependencies
npm install
# Run
npm start

Прежде чем перейти к созданию чего-либо, давайте взглянем с высоты птичьего полета на то, как будет выглядеть структура нашего приложения:

|----app
|------Comments
|--------Components
|----------comment-box.component.ts # Box
|----------comment-form.component.ts # Form
|----------comment-list.component.ts # List
|----------index.ts # Comment componens curator
|--------Model
|----------comment.ts # Comment Model (Interface/Structure)
|--------Services
|----------comment.service.ts # HTTP service
|------app.component.ts # Entry
|------emitter.service.ts #Utility service for component interaction
|------main.ts # Bootstrapper

Взаимодействие компонентов: чего вы могли не знать

Веб-компоненты великолепны, но их иерархическая природа делает управление ими довольно сложным. Некоторые компоненты настолько глупы, что все, что они могут делать, это получать данные и распространять их в представлении или генерировать события.

Это может показаться простым, потому что такие компоненты могут просто получать данные от своего родительского компонента, который может быть более умным компонентом, который знает, как обрабатывать данные. В Angular данные передаются от родителя к дочернему с помощью Входа.

Другой сценарий — когда есть изменение в дочернем компоненте, и родительский компонент должен быть уведомлен об изменении. Ключевое слово — уведомить, что означает, что дочерний элемент вызовет событие, которое прослушивает родитель. Это делается с помощью Output в Angular.

Настоящая боль возникает, когда братьям и сестрам или двоюродным братьям нужно уведомлять друг друга о внутренних изменениях. Angular не предоставляет основного решения для этого, но есть решения. Самый распространенный способ — создать центральный концентратор событий, который отслеживает события с помощью идентификатора:

/* * * ./app/emitter.service.ts * * */
// Credit to https://gist.github.com/sasxa
// Imports
import {Injectable, EventEmitter} from '@angular/core';
@Injectable()
export class EmitterService {
    // Event store
    private static _emitters: { [ID: string]: EventEmitter<any> } = {};
    // Set a new event in the store with a given ID
    // as key
    static get(ID: string): EventEmitter<any> {
        if (!this._emitters[ID]) 
            this._emitters[ID] = new EventEmitter();
        return this._emitters[ID];
    }
}

Все, что он делает, это регистрирует события в объекте _emitters и генерирует их, когда они вызываются с помощью метода get().

Фактический трюк состоит в том, чтобы установить эти идентификаторы в родительском контейнере или контейнере прародителя и передать идентификаторы каждому дочернему и внуку, который должен уведомить родителя, а затем использовать метод жизненного цикла ngOnChanges для прослушивания, когда идентификатор ткнул. Затем вы можете подписаться на эмитированное событие в ngOnChanges.

Звучит криво? Мы уточним по дороге.

Встречайте HTTP-сервис Angular 2

Прежде чем мы создадим компоненты, давайте сделаем то, ради чего мы сюда пришли и чего мы так долго ждали. Ниже приведена подпись HTTP, как в исходном коде Angular 2:

/**
     * Performs any type of http request. First argument is required, and can either be a url or
     * a {@link Request} instance. If the first argument is a url, an optional {@link RequestOptions}
     * object can be provided as the 2nd argument. The options object will be merged with the values
     * of {@link BaseRequestOptions} before performing the request.
     */
    request(url: string | Request, options?: RequestOptionsArgs): Observable<Response>;
    /**
     * Performs a request with `get` http method.
     */
    get(url: string, options?: RequestOptionsArgs): Observable<Response>;
    /**
     * Performs a request with `post` http method.
     */
    post(url: string, body: any, options?: RequestOptionsArgs): Observable<Response>;
    /**
     * Performs a request with `put` http method.
     */
    put(url: string, body: any, options?: RequestOptionsArgs): Observable<Response>;
    /**
     * Performs a request with `delete` http method.
     */
    delete(url: string, options?: RequestOptionsArgs): Observable<Response>;
    /**
     * Performs a request with `patch` http method.
     */
    patch(url: string, body: any, options?: RequestOptionsArgs): Observable<Response>;
    /**
     * Performs a request with `head` http method.
     */
    head(url: string, options?: RequestOptionsArgs): Observable<Response>;

Каждый метод принимает URL-адрес и полезную нагрузку в зависимости от обстоятельств и возвращает общий наблюдаемый тип ответа. В этом уроке нас интересуют только публиковать, помещать, получать и удалять, но приведенное выше показывает, что еще вы можете попробовать.

Класс обслуживания имеет следующую структуру:

/* * * ./app/comments/services/comment.service.ts * * */
// Imports
import { Injectable }     from '@angular/core';
import { Http, Response, Headers, RequestOptions } from '@angular/http';
import { Comment }           from '../model/comment';
import {Observable} from 'rxjs/Rx';
// Import RxJs required methods
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
@Injectable()
export class CommentService {
     // Resolve HTTP using the constructor
     constructor (private http: Http) {}
     // private instance variable to hold base url
     private commentsUrl = 'http://localhost:3000/api/comments'; 
}

Мы импортируем необходимые библиотеки, чтобы наш сервис работал должным образом. Обратите внимание, что наблюдаемая, о которой мы говорили, также была импортирована и готова к использованию. Операторы map и catch observable, которые помогут нам манипулировать данными и обрабатывать ошибки соответственно, также были импортированы. Затем мы внедряем HTTP в конструктор и сохраняем ссылку на базовый URL-адрес нашего API.

// Fetch all existing comments
     getComments() : Observable<Comment[]> {
         // ...using get request
         return this.http.get(this.commentsUrl)
                        // ...and calling .json() on the response to return data
                         .map((res:Response) => res.json())
                         //...errors if any
                         .catch((error:any) => Observable.throw(error.json().error || 'Server error'));
     }

Используя экземпляр http, который у нас уже есть в классе, мы вызываем его метод get, передавая базовый URL-адрес, потому что это конечная точка, где мы можем найти список комментариев.

Мы придерживаемся строгости, гарантируя, что методы экземпляра службы всегда возвращают наблюдаемое значение типа Comment:

/* * * ./app/comments/model/comment.ts * * */
export class Comment {
    constructor(
        public id: Date, 
        public author: string, 
        public text:string
        ){}
}

С оператором карты мы вызываем метод .json для ответа, поскольку фактический ответ представляет собой не набор данных, а строку JSON.

Всегда рекомендуется обрабатывать ошибки, чтобы мы могли использовать оператор catch для возврата другого наблюдаемого объекта подписываемого, но на этот раз неудачного.

Остальной код имеет описанную выше структуру, но другие методы и аргументы HTTP:

// Add a new comment
    addComment (body: Object): Observable<Comment[]> {
        let bodyString = JSON.stringify(body); // Stringify payload
        let headers      = new Headers({ 'Content-Type': 'application/json' }); // ... Set content type to JSON
        let options       = new RequestOptions({ headers: headers }); // Create a request option
        return this.http.post(this.commentsUrl, body, options) // ...using post request
                         .map((res:Response) => res.json()) // ...and calling .json() on the response to return data
                         .catch((error:any) => Observable.throw(error.json().error || 'Server error')); //...errors if any
    }   
    // Update a comment
    updateComment (body: Object): Observable<Comment[]> {
        let bodyString = JSON.stringify(body); // Stringify payload
        let headers      = new Headers({ 'Content-Type': 'application/json' }); // ... Set content type to JSON
        let options       = new RequestOptions({ headers: headers }); // Create a request option
        return this.http.put(`${this.commentsUrl}/${body['id']}`, body, options) // ...using put request
                         .map((res:Response) => res.json()) // ...and calling .json() on the response to return data
                         .catch((error:any) => Observable.throw(error.json().error || 'Server error')); //...errors if any
    }   
    // Delete a comment
    removeComment (id:string): Observable<Comment[]> {
        return this.http.delete(`${this.commentsUrl}/${id}`) // ...using put request
                         .map((res:Response) => res.json()) // ...and calling .json() on the response to return data
                         .catch((error:any) => Observable.throw(error.json().error || 'Server error')); //...errors if any
    }

Вышеприведенное делает публикацию, запрос на размещение и удаление, преобразует ответ в JSON и перехватывает ошибку, если таковая имеется. Теперь вы видите, наблюдаемые не так многословны, как казалось вначале. Осталось только подписаться на наблюдаемое и привязать данные по мере их отправки к представлениям. Давайте создадим наши компоненты.

Компоненты

Время связать вещи вместе. Теперь, когда эмиттер и служба данных отключены, мы можем создавать компоненты, которые связывают их вместе, чтобы создать пригодное для использования приложение.

Поле для комментариев

Поле для комментариев — сердце нашего приложения. Он содержит примитивные детали, которые включают автора комментария и текст комментария:

/* * * ./app/comments/components/comment-box.component.ts * * */
// Imports
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Comment } from '../model/comment'
import { EmitterService } from '../../emitter.service';
import { CommentService } from '../services/comment.service';
// Component decorator
@Component({
    selector: 'comment-box',
    template: `
       <!-- Removed for brevity 'ssake -->
    `
    // No providers here because they are passed down from the parent component
})
// Component class
export class CommentBoxComponent { 
    // Constructor
     constructor(
        private commentService: CommentService
        ){}
    // Define input properties
    @Input() comment: Comment;
    @Input() listId: string;
    @Input() editId:string;
    editComment() {
        // Emit edit event
        EmitterService.get(this.editId).emit(this.comment);
    }
    deleteComment(id:string) {
        // Call removeComment() from CommentService to delete comment
        this.commentService.removeComment(id).subscribe(
                                comments => {
                                    // Emit list event
                                    EmitterService.get(this.listId).emit(comments);
                                }, 
                                err => {
                                    // Log errors if any
                                    console.log(err);
                                });
    }
}

Свойство комментария, украшенное @Input, содержит данные, переданные из родительского компонента в компонент поля комментария. При этом мы можем получить доступ к свойствам автора и текста, которые будут отображаться в представлении. Два метода, editComment и deleteComment, как следует из названия, загружают форму с комментарием для обновления или удаления комментария соответственно.

editComment выдает комментарий к редактированию, который отслеживается по входному идентификатору. Вы уже могли догадаться, что это событие прослушивает компонент формы комментариев. Метод deleteComment вызывает метод removeComment для экземпляра CommentService, чтобы удалить комментарий. Как только это успешно, он генерирует событие списка для компонента списка комментариев, чтобы обновить его данные.

Полезная нагрузка передается событиям, которые подписчик может получить. Мы не должны передавать фактические данные, вместо этого мы можем использовать простой флаг, указывающий на то, что изменение было внесено, а затем получать данные с помощью соответствующего компонента

<div class="panel panel-default">
            <div class="panel-heading">{{comment.author}}</div>
            <div class="panel-body">
                {{comment.text}}
            </div>
            <div class="panel-footer">
                <button class="btn btn-info" (click)="editComment()"><span class="glyphicon glyphicon-edit"></span></button>
                <button class="btn btn-danger" (click)="deleteComment(comment.id)"><span class="glyphicon glyphicon-remove"></span></button>
            </div>
        </div>

Используйте кнопки для привязки событий редактирования и удаления комментариев к представлению. Вышеупомянутый фрагмент был удален из компонента поля комментариев для краткости

Форма комментария

Форма комментариев будет состоять из текстового поля для автора, текстовой области для текста и кнопки для внесения изменений:

<form (ngSubmit)="submitComment()">
            <div class="form-group">
                <div class="input-group">
                    <span class="input-group-addon" id="basic-addon1"><span class="glyphicon glyphicon-user"></span></span>
                    <input type="text" class="form-control" placeholder="Author" [(ngModel)]="model.author" name="author">
                </div>
                <br />
                <textarea class="form-control" rows="3" placeholder="Text" [(ngModel)]="model.text" name="text"></textarea>
                <br />
                <button *ngIf="!editing" type="submit" class="btn btn-primary btn-block">Add</button>
                <button *ngIf="editing" type="submit" class="btn btn-warning btn-block">Update</button>
            </div>
        </form>

На самом деле есть две кнопки, но одна может отображаться одновременно, а другая скрыта. Такое поведение распространено. Мы просто переключаемся между режимом редактирования или режимом создания.

/* * * ./app/comments/components/comment-form.component.ts * * */
// Imports
import { Component, EventEmitter, Input, OnChanges } from '@angular/core';
import { NgForm }    from '@angular/common';
import {Observable} from 'rxjs/Rx';
import { CommentBoxComponent } from './comment-box.component'
import { CommentService } from '../services/comment.service';
import { EmitterService } from '../../emitter.service';
import { Comment } from '../model/comment'
// Component decorator
@Component({
    selector: 'comment-form',
    template: `
       <!-- Removed for brevity, included above -->
    `,
    providers: [CommentService]
})
// Component class
export class CommentFormComponent implements OnChanges { 
    // Constructor with injected service
    constructor(
        private commentService: CommentService
        ){}
    // Local properties
    private model = new Comment(new Date(), '', '');
    private editing = false;
    // Input properties
     @Input() editId: string;
     @Input() listId: string;
    submitComment(){
        // Variable to hold a reference of addComment/updateComment
        let commentOperation:Observable<Comment[]>;
        if(!this.editing){
            // Create a new comment
            commentOperation = this.commentService.addComment(this.model)
        } else {
            // Update an existing comment
             commentOperation = this.commentService.updateComment(this.model)
        }
        // Subscribe to observable
        commentOperation.subscribe(
                                comments => {
                                    // Emit list event
                                    EmitterService.get(this.listId).emit(comments);
                                    // Empty model
                                    this.model = new Comment(new Date(), '', '');
                                    // Switch editing status
                                    if(this.editing) this.editing = !this.editing;
                                }, 
                                err => {
                                    // Log errors if any
                                    console.log(err);
                                });
    }
    ngOnChanges() {
        // Listen to the 'edit'emitted event so as populate the model
        // with the event payload
        EmitterService.get(this.editId).subscribe((comment:Comment) => {
            this.model = comment
            this.editing = true;
        });
    }
 }

Существует свойство модели для отслеживания данных в форме. Модель меняется в зависимости от состояния приложения. При создании нового комментария он пуст, но при редактировании он заполняется данными для редактирования.

Метод ngOnChanges отвечает за переключение в режим редактирования путем установки для свойства редактирования значения true после загрузки свойства модели с комментарием для обновления.

Этот комментарий извлекается путем подписки на событие редактирования, которое мы создали ранее.

Помните, что метод ngOnChanges вызывается при изменении любого входногосвойства компонента

Список комментариев

Список комментариев довольно прост, он просто перебирает комментарий списка и передает данные в поле комментария:

/* * * ./app/comments/components/comment-list.component.ts * * */
// Imports
import { Component, OnInit, Input, OnChanges } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { CommentBoxComponent } from './comment-box.component';
import { Comment } from '../model/comment';
import {CommentService} from '../services/comment.service';
import { EmitterService } from '../../emitter.service';
// Component decorator
@Component({
    selector: 'comment-list',
    template: `
        <comment-box 
        [editId]="editId" 
        [listId]="listId" 
        *ngFor="let comment of comments" 
        [comment]="comment">
    </comment-box>
    `,
    directives: [CommentBoxComponent],
    providers: [CommentService]
})
// Component class
export class CommentListComponent implements OnInit, OnChanges{
    // Local properties
    comments: Comment[];
    // Input properties
    @Input() listId: string;
    @Input() editId: string;
    // Constructor with injected service
    constructor(private commentService: CommentService) {}
    ngOnInit() {
            // Load comments
            this.loadComments()
    }
    loadComments() {
        // Get all comments
         this.commentService.getComments()
                           .subscribe(
                               comments => this.comments = comments, //Bind to view
                                err => {
                                    // Log errors if any
                                    console.log(err);
                                });
    }
    ngOnChanges(changes:any) {
        // Listen to the 'list'emitted event so as populate the model
        // with the event payload
        EmitterService.get(this.listId).subscribe((comments:Comment[]) => { this.loadComments()});
    }
}

Он также реализует OnInit и OnChanges. Переопределяя ngOnInit, мы можем загружать существующие комментарии из API, а переопределяя ngOnChanges, мы можем перезагружать комментарии при удалении, создании или обновлении комментария.

Обратите внимание, что событие, на которое мы подписываемся, на этот раз является событием списка, которое генерируется в компоненте формы комментария при создании нового комментария или обновлении существующего комментария. Он также генерируется в компоненте поля комментария при удалении комментария.

Указатель комментариев

Это один просто куратор. Он собирает все компоненты комментариев и экспортирует их для импорта компонента приложения:

/* * * ./app/comments/components/index.ts * * */
// Imports
import { Component} from '@angular/core';
import { CommentFormComponent } from './comment-form.component'
import { CommentListComponent } from './comment-list.component'
import {EmitterService} from '../../emitter.service';
@Component({
    selector: 'comment-widget',
    template: `
        <div>
            <comment-form [listId]="listId" [editId]="editId"></comment-form>
            <comment-list [listId]="listId" [editId]="editId"></comment-list>
        </div>
    `,
    directives: [CommentListComponent, CommentFormComponent],
    providers: [EmitterService]
})
export class CommentComponent { 
    // Event tracking properties
    private listId = 'COMMENT_COMPONENT_LIST';
    private editId = 'COMMENT_COMPONENT_EDIT';
 }

Теперь вы видите, откуда взялись свойства, которые мы передавали.

Компонент приложения

Обычная точка входа в приложение Angular 2, которое, если у вас есть приложение NG2, вы бы узнали. Ключевое отличие в том, что мы добавляем к нему виджет комментария:

/* * * ./app/comments/app.component.ts * * */
// Imports
import { Component } from '@angular/core';
import { CommentComponent } from './comments/components/index'
@Component({
    selector: 'my-app',
    template: `
        <h1>Comments</h1>
        <comment-widget></comment-widget>
        `,
        directives:[CommentComponent]
})
export class AppComponent { }

Начальная загрузка

Мы загружаем приложение, предоставляя ему важный провайдер, который является HTTP_PROVIDER:

import { bootstrap }    from '@angular/platform-browser-dynamic';
import { HTTP_PROVIDERS } from '@angular/http';
import { AppComponent } from './app.component';
bootstrap(AppComponent, [HTTP_PROVIDERS]);

Метод начальной загрузки принимает аргументы option, которые используются для внедрения провайдеров.

Как выглядит наше приложение

Заворачивать

Мы начали с основной цели: обработка HTTP-запросов с наблюдаемыми. К счастью, оказалось, что мы достигли своей цели, а также получили дополнительные знания о взаимодействии компонентов и о том, почему вы должны предпочесть наблюдаемые объекты обещаниям.

Предложить для вас

Angular 2 и NodeJS — Практическое руководство по MEAN Stack 2.0

Изучите разработку на Angular 2, создав 10 приложений

Angular 2 — Полное руководство (обновлено до RC4!)

Angular 2 с TypeScript для начинающих: практическое руководство

Основы Angular 2: учитесь, создавая настоящее веб-приложение

источник: https://codequs.com/p/By-T81AO/angular-2-tutorial-http-requests-with-observables/