Bài viết được dịch từ: barbarianmeetscoding.com

Đây là bài thứ 6 (bài cuối) trong loạt bài viết: "Bắt đầu với Angular 2 từng bước một". Bạn có thể xem lại các bài viết trước ở đây:

Hôm nay chúng ta sẽ kết thúc loạt bài giới thiệu về Angular 2, bằng cách xem làm thế nào để nhận dữ liệu thực từ một web service sử dụng module http trong Angular2. Chúng ta cũng học một chút về ObservablesRxjs (Một lựa chọn mới để xử lý async).

Code mẫu

Bạn có thể tải toàn bộ code mẫu ở GitHub repo.

Ứng dụng của chúng ta cho tới thời điểm này

Hiện tại, chúng ta đã xây dựng một ứng dụng nhỏ về các nhân vật trong vũ trụ StarWars. Với 2 component, PeopleListComponent hiển thị một danh sách các nhân vật và PersonDetailsComponent hiển thị thông tin chi tiết về nhân vật được chọn. Thông tin chi tiết của nhân vật được hiển thị trong một form, bạn có thể thoải mái chỉnh sửa và lưu các thay đổi.

Chúng ta sử dụng Angular 2 routing để điều hướng giữa 2 view, danh sách các nhân vật là view mặc định và render ngay sau khi ứng dụng khởi động.

Nhận dữ liệu thật cho ứng dụng chủa chúng ta

Ở những bài trước, ứng dụng của chúng ta nhận dữ liệu sử dụng PeopleService, cái chỉ truy cập tới một mảng Person lưu trữ trong memory. Nó trông như thế này:

import { Injectable } from '@angular/core';
import { Person } from './person';

const PEOPLE : Person[] = [
      {id: 1, name: 'Luke Skywalker', height: 177, weight: 70, profession: ''},
      {id: 2, name: 'Darth Vader', height: 200, weight: 100, profession: ''},
      {id: 3, name: 'Han Solo', height: 185, weight: 85, profession: ''},
    ];

@Injectable()
export class PeopleService{

  getAll() : Person[] {
    return PEOPLE.map(p => this.clone(p));
  }
  get(id: number) : Person {
    return this.clone(PEOPLE.find(p => p.id === id));
  }
  save(person: Person){
    let originalPerson = PEOPLE.find(p => p.id === person.id);
    if (originalPerson) Object.assign(originalPerson, person);
    // saved muahahaha
  }

  private clone(object: any){
    // hack
    return JSON.parse(JSON.stringify(object));
  }
}

Trong phần còn lại, chúng ta sẽ nhận dữ liệu từ một web service thực sự. Chúng ta sẽ sử dụng Star Wars API và Angular 2 http module. Nhưng trước hết chúng ta cần cho phép nó.

Tham khảo các khóa học lập trình online, onlab, và thực tập lập trình tại TechMaster

Cho phép module http trong ứng dụng của bạn

Angular 2 http module ở trong một file riêng biệt và nó dựa vào thư viện RxJS. Điều đó có nghĩa là bạn có thể sử dụng bất kỳ thư viện http khác để khai thác các service thông qua HTTP. File cấu hình system.js chứa tất cả các thông tin bạn cần, để dễ dàng import cả 2 module http và rxjs. Nếu bạn xem file systemjs.config.js bạn sẽ thấy các tham chiếu tới cả 2 module này:

var map = {
    'app':                        'app', // 'dist',

    '@angular':                   'node_modules/@angular',
    'angular2-in-memory-web-api': 'node_modules/angular2-in-memory-web-api',
    'rxjs':                       'node_modules/rxjs'
};

// packages tells the System loader how to load when no filename and/or no extension
var packages = {
    'app':                        { main: 'main.js',  defaultExtension: 'js' },
    'rxjs':                       { defaultExtension: 'js' },
    'angular2-in-memory-web-api': { main: 'index.js', defaultExtension: 'js' },
};

// info on angular 2 modules
var ngPackageNames = [
    'common',
    'compiler',
    'core',
    'forms',
    'http',                     // <===== http module here
    'platform-browser',
    'platform-browser-dynamic',
    'router',
    'router-deprecated',
    'upgrade',
];

// etc

Để có thể sử dụng module @angular/http trong ứng dụng bạn sẽ cần thêm nó tới decorator NgModule trong file app.modules.ts. Bắt đầu bằng import HttpModule:

import { HttpModule } from '@angular/http';

Và sau đó thêm vào thuộc tính imports:

@NgModule({
  imports: [ BrowserModule, routing, FormsModule, HttpModule],
  declarations: [ AppComponent, PeopleListComponent, PersonDetailsComponent],
  bootstrap: [ AppComponent ]
})

File app.module.ts sẽ trông như thế này:

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';

import { routing } from './app.routes';

import { AppComponent }  from './app.component';
import { PeopleListComponent } from './people-list.component';
import { PersonDetailsComponent } from './person-details.component';

@NgModule({
  imports: [ BrowserModule, routing, FormsModule, HttpModule],
  declarations: [ AppComponent, PeopleListComponent, PersonDetailsComponent],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

Và bây giờ chúng ta có thể sử dụng module http để làm một số thứ!

Trong PeopleService chúng ta thêm đoạn code sau:

import { Http, Response, Headers } from '@angular/http';
import { Observable } from 'rxjs/Rx';

Http service cung cấp API để tạo ra các HTTP request với các phương thức phù hợp với các phương thức của HTTP như: get, post, put, ...

Response biểu diễn một response từ một HTTP service và tuân theo các thông số kỹ thuật của API.

Observable là một async pattern được sử dụng trong Angular 2. Khái niệm observable đến từ observer design pattern là một đối tượng thông báo cho các đối tượng theo dõi khi một điều gì đó xảy ra. Trong RxJs nói chung là quản lý chuỗi các dữ liệu hoặc sự kiện và cung cấp nhiều hàm tiện ích.

Sau khi import các module cần thiết chúng ta có thể tiêm Http service thông qua hàm khởi tạo của PeopleService:

@Injectable()
export class PeopleService{
  constructor(private http : Http){
  }
  // other methods...
}

Bây giờ chúng ta sẽ cập nhật phương thức getAll như thế này:

Injectable()
export class PeopleService{
  private baseUrl: string = 'http://swapi.co/api';
  constructor(private http : Http){
  }

  getAll(): Observable<Person[]>{
    let people$ = this.http
      .get(`${this.baseUrl}/people`, {headers: this.getHeaders()})
      .map(mapPersons);
      return people$;
  }

  private getHeaders(){
    let headers = new Headers();
    headers.append('Accept', 'application/json');
    return headers;
  }

  // other code...
}

Nếu bạn đã làm việc với Angular 1 http service, hoặc đã quen với việc sử dụng promise đoạn code này có thể trông khá lạ với bạn. map làm gì ở đây? Sao không có then?

Phương thức http.get tạo ra một GET request tới Star Wars API và trả lại một Observable<Response> (không phải là một Promise như bạn mong đợi). Observable<Response> có thể mô tả như một chuỗi các response theo thời gian, trong trường hợp này nó là một chuỗi của một response duy nhất.

Xem nó như một chuỗi hoặc tập hợp của các response theo thời gian, làm cho nó có ý nghĩa hơn khi sử dụng một phương thức giống như map để chuyển đổi các item trong chuỗi thành các loại dữ liệu sử dụng trong ứng dụng, ví dụ như các nhân vật.

Nếu chúng ta nghĩ như vậy, chúng ta có thể định nghĩa hàm mapPersons để chuyển đổi một Response thành một mảng các nhân vật như dưới đây:

function mapPersons(response:Response): Person[]{
   // The response of the API has a results
   // property with the actual results
   return response.json().results.map(toPerson)
}

function toPerson(r:any): Person{
  let person = <Person>({
    id: extractId(r),
    url: r.url,
    name: r.name,
    weight: r.mass,
    height: r.height,
  });
  console.log('Parsed person:', person);
  return person;
}

// to avoid breaking the rest of our app
// I extract the id from the person url
function extractId(personData:any){
 let extractedId = personData.url.replace('http://swapi.co/api/people/','').replace('/','');
 return parseInt(extractedId);
}

Bây giờ chúng ta sẽ trả lại một Observable<Person[]> từ phương thức getAll, chúng ta cần cập nhật PeopleListComponent. Làm thế nào bạn có thể sử dụng mảng các nhân vật từ Observable<Person[]>? Bạn có thể sử dụng subscribe để làm điều này:

@Component({...})
export class PeopleListComponent implements OnInit{
  people: Person[] = [];

  constructor(private peopleService : PeopleService){}

  ngOnInit(){
    this._peopleService
      .getAll()
      .subscribe(p => this.people = p)
  }
}

Trông nó khá giống phương thức then, trong một promise đúng không?

Chúng ta tiếp tục với phương thức get:

Injectable()
export class PeopleService{
  // code ...

  get(id: number): Observable<Person> {
    let person$ = this.http
      .get(`${this.baseUrl}/people/${id}`, {headers: this.getHeaders()})
      .map(mapPerson);
      return person$;
  }
}

function mapPerson(response:Response): Person{
   // toPerson looks just like in the previous example
   return toPerson(response.json());
}

Và phương thức save:

Injectable()
export class PeopleService{
  // code ...

  save(person: Person) : Observable<Response>{
    // this won't actually work because the StarWars API doesn't 
    // is read-only. But it would look like this:
    return this
      .http
      .put(`${this.baseUrl}/people/${person.id}`, JSON.stringify(person), {headers: this.getHeaders()});
  }
}

Khi chúng ta thay đổi các phương thức public trong PeopleService chúng ta sẽ cần cập nhật PersonDetailsComponent, component sử dụng chúng. Một lần nữa, chúng ta sử dụng subscribe:

@Component({...})
export class PersonDetailsComponent implements OnInit, OnDestroy {
    person: Person;
    sub: any;
    professions: string[] = ['jedi', 'bounty hunter', 'princess', 'sith lord'];

    constructor(private peopleService: PeopleService,
                private route: ActivatedRoute,
                private router: Router){
    }

    ngOnInit(){
        this.sub = this.route.params.subscribe(params => {
          let id = Number.parseInt(params['id']);
          console.log('getting person with id: ', id);
          this.peopleService
            .get(id)
            .subscribe(p => this.person = p);
        });
    }

    ngOnDestroy(){
        this.sub.unsubscribe();
    }

    gotoPeoplesList(){
        let link = ['/persons'];
        this.router.navigate(link);
    }

    savePersonDetails(){
      this.peopleService
          .save(this.person)
          .subscribe(
            (r: Response) => {console.log('success');}
          );
    }
}

Bây giờ bạn có thể chạy ứng dụng (npm start) và xem kết quả.

Đây là file people.service.ts hoàn chỉnh để tham khảo:

import { Injectable } from '@angular/core';
import { Http, Response, Headers } from '@angular/http';
import { Observable } from 'rxjs/Rx';
import { Person } from './person';

@Injectable()
export class PeopleService{
  private baseUrl: string = 'http://swapi.co/api';

  constructor(private http : Http){
  }

  getAll(): Observable<Person[]>{
    let people$ = this.http
      .get(`${this.baseUrl}/people`, {headers: this.getHeaders()})
      .map(mapPersons);
      return people$;
  }

  get(id: number): Observable<Person> {
    let person$ = this.http
      .get(`${this.baseUrl}/people/${id}`, {headers: this.getHeaders()})
      .map(mapPerson);
      return person$;
  }

  save(person: Person) : Observable<Response>{
    // this won't actually work because the StarWars API doesn't 
    // is read-only. But it would look like this:
    return this.http
      .put(`${this.baseUrl}/people/${person.id}`, JSON.stringify(person), {headers: this.getHeaders()});
  }

  private getHeaders(){
    let headers = new Headers();
    headers.append('Accept', 'application/json');
    return headers;
  }
}

function mapPersons(response:Response): Person[]{
   // The response of the API has a results
   // property with the actual results
   return response.json().results.map(toPerson)
}

function toPerson(r:any): Person{
  let person = <Person>({
    id: extractId(r),
    url: r.url,
    name: r.name,
    weight: r.mass,
    height: r.height,
  });
  console.log('Parsed person:', person);
  return person;
}

// to avoid breaking the rest of our app
// I extract the id from the person url
function extractId(personData:any){
 let extractedId = personData.url.replace('http://swapi.co/api/people/','').replace('/','');
 return parseInt(extractedId);
}

function mapPerson(response:Response): Person{
  // toPerson looks just like in the previous example
  return toPerson(response.json());
}

Điều gì xảy ra khi response là một lỗi?

Xử lý lỗi với observable

Observable giống như Promise, có một cách đơn giản để xử lý lỗi. PeopleService, PersonDetailsComponentPeopleListComponent làm việc ở 2 mức trừu tượng khác nhau (một là các response + HTTP request và cái còn lại là với các object), chúng ta có 2 mức xử lý lỗi.

Mức đầu tiên của xử lý lỗi là ở mức độ service, đó là các vấn đề có thể xảy ra với các HTTP request. Trong ứng dụng đơn giản này nó sẽ log ra lỗi và chuyển nó thành một lỗi mức ứng dụng.

Injectable()
export class PeopleService{
  // code ...

  get(id: number): Observable<Person> {
    let person$ = this._http
      .get(`${this.baseUrl}/people/${id}`, {headers: this.getHeaders()})
      .map(mapPerson)
      .catch(handleError);
      return person$;
  }

}

// this could also be a private method of the component class
function handleError (error: any) {
  // log error
  // could be something more sofisticated
  let errorMsg = error.message || `Yikes! There was was a problem with our hyperdrive device and we couldn't retrieve your data!`
  console.error(errorMsg);

  // throw an application level error
  return Observable.throw(errorMsg);
}

Mức xử lý lỗi thứ 2 xảy ra tại mức ứng dụng bên trong component:

@Component({
  selector: 'people-list',
  template: `
  <section>
    <ul>
      <!-- this is the new syntax for ng-repeat -->
      <li *ngFor="let person of people">
          <a href="#" [routerLink]="['/persons', person.id]">
        {{person.name}}
        </a>
      </li>
    </ul>
    <section *ngIf="errorMessage">
      {{errorMessage}}
    </section>
  </section>
  `
})
export class PeopleListComponent implements OnInit{
  people: Person[] = [];
  errorMessage: string = '';

  constructor(private peopleService : PeopleService){ }

  ngOnInit(){
    this.peopleService
      .getAll()
      .subscribe(
         /* happy path */ p => this.people = p,
         /* error path */ e => this.errorMessage = e);
  }
}

Đây là nơi chúng ta hiển thị lỗi tới người dùng.

Ngoài ra, phương thức subscribe giúp bạn định nghĩa một tham số thứ 3 với một hàm onComplete để thực thi khi một request đã hoàn thành. Chúng ta có thể sử dụng nó, ví dụ để ẩn hoặc hiện một progress spinner hoặc một thông điệp loading:

import { Component, OnInit } from '@angular/core';
import { Person } from './person';
import { PeopleService } from './people.service';

@Component({
  selector: 'people-list',
  template: `
  <section>
    <section *ngIf="isLoading && !errorMessage">
    Loading our hyperdrives!!! Retrieving data...
    </section>
      <ul>
        <!-- this is the new syntax for ng-repeat -->
        <li *ngFor="let person of people">
            <a href="#" [routerLink]="['/persons', person.id]">
          {{person.name}}
          </a>
        </li>
      </ul>
      <section *ngIf="errorMessage">
        {{errorMessage}}
      </section>
  </section>
  `
})
export class PeopleListComponent implements OnInit{
  people: Person[] = [];
  errorMessage: string = '';
  isLoading: boolean = true;

  constructor(private peopleService : PeopleService){ }

  ngOnInit(){
    this.peopleService
      .getAll()
      .subscribe(
         /* happy path */ p => this.people = p,
         /* error path */ e => this.errorMessage = e,
         /* onComplete */ () => this.isLoading = false);
  }
}

Chúng ta có thể kiểm tra việc xử lý lỗi bằng giả lập một lỗi trong PeopleService. Hãy throw một lỗi trong hàm mapPersons:

function mapPersons(response:Response): Person[]{
   throw new Error('ups! Force choke!');

   // The response of the API has a results
   // property with the actual results
   return response.json().results.map(toPerson)
}

Nếu bây giờ bạn chạy ứng dụng (npm start) bạn sẽ thấy cả console và UI hiển thị thông điệp sau: ups! Force choke!.

Bây giờ bạn có thể xóa lỗi giả đi và tiếp tục. Sẽ tốt hơn nếu chúng ta có thể đẩy dữ liệu trực tiếp tới template bất kỳ khi nào chúng có sẵn? Thay vì phải làm thủ công bằng cách gọi subscribe và thiết lập thuộc tính people?

Chúng ta có thể làm điểu đó với async pipe!

Async pipe

Sử dụng async pipe (pipe chỉ là tên gọi mới của filter trong Angular 1) bạn có thể đơn giản hóa ví dụ trên bằng cách:

Khai báo thuộc tính people: Observable<Person[]>trong component của bạn

Thiết lập giá trị của nó sử dụng phương thức peopleService.getAll

Sử dụng async pipe trong template

@Component({
  selector: 'people-list',
  template: `
  <ul class="people">
    <li *ngFor="let person of people | async " >
        <a href="#" [routerLink]="['/persons', person.id]">
          {{person.name}}
        </a>
    </li>
  </ul>
  `,
})
export class PeopleListComponent implements OnInit{
  people: Observable<Person[]>;

  constructor(private peopleService : PeopleService){}

  ngOnInit(){
    this.people = this.peopleService.getAll();
  }
}

Điều này đơn giản hóa logic của PeopleListComponent rất nhiều và làm cho code bất đồng bộ trông giống như đồng bộ.

Chú ý, tôi đã xóa hàm xử lý lỗi và thông báo is loading ... vì mục đích đơn giản. 

Chuyển observable thành promise

Thư viện RxJS cung cấp một phương thức để chuyển observable thành promise đó là toPromise.

Chúng ta có thể viết lại phương thức getAll như sau:

Injectable()
export class PeopleService{
  private baseUrl: string = 'http://swapi.co/api';
  constructor(private http : Http){}

  getAll(): Promise<Person>{
    return this.http
      .get(`${this.baseUrl}/people`, {headers: this.getHeaders()})
      .toPromise()
      .then(mapPersons)
      .catch(handleError);
  }

  // other code...
}

function handleError(error: any){
  // log error
  // could be something more sofisticated
  let errorMsg = error.message || `Yikes! There was was a problem with our hyperdrive device and we couldn't retrieve your data!`
  console.error(errorMsg);
  // instead of Observable we return a rejected Promise
  return Promise.reject(errorMsg);
}

Bạn có thể xử lý promise được trả lại, từ bất kỳ component giống như trong Angular 1 và các framework khác.

Bạn muốn đọc nhiều hơn về Angular 2 http và RxJS?

Kết luận

Ngày hôm nay, bạn đã học thêm một cách xử lý bất đồng bộ trong Angular 2, observable và cách nó được sử dụng bởi module http mới. Bạn cũng học cách xử lý lỗi với observable và cách tận dụng lợi thế của async pipe để đơn giản hóa code của component. Cuối cùng bạn đã khám phá cách bạn có thể tiếp tục sử dụng promise nếu muốn, bằng cách sử dụng phương thức toPromise.

Đây là bài viết cuối cùng trong loạt bài viết hướng dẫn về Angular 2. Tôi thực sự hy vọng rằng bạn thích nó!