Consume API - czyli jak połączyć się z naszym API z Angulara.
Angular 4
Po pierwsze w celu wyjaśnienia. Mimo iż numeracja jest taka ciekawa, że mamy już Angulara 4 to tak na prawdę nie ma tutaj nic ciekawego. Napisane na nowszej wersji TypeScripta, rozszerzona dyrektywa ngIf i ngFor oraz poprawione animacji. A aktulizacja bardzo prosta:
npm update
I mamy :) A dokładniej to najlepiej jeśli zastosujemy się do instrukcji to powinniśmy zrobić to tak:
npm install @angular/common@latest @angular/compiler@latest @angular/compiler-cli@latest @angular/core@latest @angular/forms@latest @angular/http@latest @angular/platform-browser@latest @angular/platform-browser-dynamic@latest @angular/platform-server@latest @angular/router@latest @angular/animations@latest typescript@latest --save
Oczywiście zastosujmy tylko te paczki, których używamy. W przypadku tego projektu - wszystko zadziałało poprawnie.
Do rzeczy - API
Dokładnie. Przejdźmy do rzeczy. Zaczynam jak zawsze od widoku. Jako iż delikatnie koncepcja rejestracj iisę zmieniła i utworzyła tak na prawdę podczas tworzenia API, przyszedł czas na utworzenie widoku.
View first
<div class="login-page">
<h1>MassCo</h1>
<span class="subtitle">Talk with coworkers around the world with MassCo!</span>
<input placeholder="Cellphone number" type="tel" class="mc-input">
<div class="step-container first-step">
<button class="mc-button next-button">next</button>
</div>
<div class="step-container login-step">
<label for="security-code">Provide security code from text message we send you:</label>
<input placeholder="Enter security code" maxlength="4" type="number" class="mc-input" name="security-code">
<button class="mc-button login-button">confirm</button>
</div>
<div class="step-container register-step">
<input placeholder="Username" type="text" class="mc-input">
<button class="mc-button register-button">create account</button>
</div>
<div class="step-container">
<button class="mc-button next-button">back</button>
</div>
</div>
Kilka zmian się pojawiło. Po pierwsze doszła zawartość do fragmentu z rejestracją. Pole Username oraz przycisk create account. Doszedł też nowy krok - potwierdzenie (confirm). Mimo iż jest to z poziomu widoku to samo - wymaganie narzuciła implementacja komponentu. Także przejdźmy do niej.
login.service.ts
Najpierw kod - potem tłumaczenia:
import { Injectable } from '@angular/core';
import { Http, Headers, Response } from "@angular/http";
import { Observable } from "rxjs/Rx";
import { ConfirmVM, RegisterVM, ValidateVM } from './models';
import "rxjs/add/operator/do";
import "rxjs/add/operator/map";
@Injectable()
export class LoginService {
API_URL: string;
constructor(private http: Http) {
this.API_URL = 'http://massco.api:56169/api';
}
validate(request: ValidateVM) {
let url = this.API_URL + "/Account/validate";
return this.http.post(url, request)
.map(response => response.json())
.catch(error => {
return Observable.throw(error);
});
}
createAccount(request: RegisterVM) {
let url = this.API_URL + "/Account";
return this.http.post(url, request)
.map(response => response.json())
.catch(error => {
return Observable.throw(error);
});
}
confirmAccount(request: ConfirmVM) {
let url = this.API_URL + "/Account/confirm";
return this.http.put(url, request)
.catch(error => {
return Observable.throw(error);
});
}
authenticate(request : ConfirmVM) {
let headers = new Headers();
headers.append('Content-Type', 'application/x-www-form-urlencoded');
let url = this.API_URL + "/token";
let body = `accountId=${request.accountId}&confirmationCode=${request.confirmationCode}`
return this.http.post(url, body, { headers: headers })
.map(response => response.json())
.catch(error => {
return Observable.throw(error);
});
}
}
Do łatwiejszej implementacji wykorzystałem reactive extensions for JS. Bardzo przydatna biblioteka. Zaraz sami się przekonacie. Każdy serwis w angularze powinien być opatrzony dekoratorem @Injectable(). Dzięki temu serwis bedzie mozna wstrzyknąć do naszego komponentu. W konstruktorze wstrzykujemy zależność do Http oraz ustawiamy adres bazowy do API. potem kolejne metody. Generalnie każda jest bardzo podobna. Ustawiamy adres, wykonujemy posta, mapujemy odpowiedź z JSONa na obiekt i przechwytujemy wyjątek. Każda taka operacja jest właśnie z RxJS. Mapowanie - polega na odczytywaniu wyniku i zmapowanie na potrzebny przez nas wynik. Catch używany jest do przechwytywania błędnej odpowiedzi. Są jeszcze takie metody jak np. do, która umożliwia wykonanie określonej akcji. Warto generalnie zapoznać się z bilioteką RxJS i korzystać jak najwięcej - dużo praktycznych możliwości.
W metodzie Authenticate dodatkowo nasze API wymagało aby zmienić typ requesta. Możecie sami zobacyzć jak zmodyfikowane zostały nagłówki i jak przesłać poprawnie treść w takim wypadku.
Komponent
PRzyszedł czas na niemalże ostatni krok. Jeśli chcemy użyć naszego nowo utworzonego serwisu musimy o tym powiadomić komponent za pomocą wpisu do tablicy providers:
@Component({
selector: 'login-app',
templateUrl: './login.component.html',
styleUrls: [ './login.component.less' ],
providers: [
LoginService
]
})
Dodałem oczywiście też nowy krok, o którym wspomniałem i dodałem nowe modele do przechowywania i bindowania danych. Następnie kilka metod: login, register, confirm, checkPhone, confirmAccount. Jedna z metod wygląda np. tak:
checkPhone() {
var self = this;
var request = new ValidateVM();
request.phoneNumber = self.validateVM.phoneNumber;
this.loginService
.validate(request)
.subscribe((data) => {
self.confirmVm = new ConfirmVM();
self.confirmVm.accountId = data.accountId;
self.currentStep = self.steps.login;
}, (error) => {
// PhoneNumber NotFound
if (error.status == 404) {
self.registerVm = new RegisterVM();
self.registerVm.phoneNumber = request.phoneNumber;
self.currentStep = self.steps.register;
}
});
}
Warto na początku zawsze pod kolakną zmienną przepisać wskazanie na this. Ze względu na to iż this wskazuje na inny obiekt w różnych kontekstach. Dzięki temu mamy pewność, ze będziemy mieli dostęp do komponentu w każdym momencie. Następnie przygotowujemy obiekt requestu i wywołujemy metodę validate() z LoginService. I tutaj ponownie przychodzi nam z pomocą RxJS i kolekcja Observable. Podpinamy się do wyniku za pomocą metody subscribe(), która jako pierwszy parametr przyjmuje akcję “pozytywną”, a drugi parametr to akcja “błędu”. W naszym wypadku akcja błędu dodatkowo jest obarczona logiką.
Pozostałe metody są zaimplementowane w podobny sposób - możecie sami zobaczyć w repozytorium.
Sprawdzenie w działaniu
No dobrze. Przyszedł czas na pierwszą próbę. Uruchamiam. Wprowadzam numer do logowania i… Not found. WHAT?! No to ja szukam co złego jest w tym numerze telefonu. Sprawdzam bazę - ok, sprawdzam kod - ok, podłącam debuggera - nie wykonuje się! WTF? Postman - DZIAŁA! Szybkie googlanie - co może być nie tak? Co się okazuje - electron nie pozwala na łączenie z adresami lokalnymi (localhost, 127.0.0.1). No to szybko do pliku hosts za pomocą programu Hosts File Editor wprowadzam host name dla lokalnego adresu. Mój nowy adres to http://massco.api:56169/.
Czas na kolejną próbę. Uruchamiam i znów… Bad request. Ok. Kolejna przeszkoda. Tym razem nie szukałem nawet przyczyny w kodzie. Próbuję w przeglądarce dostać się do dokumentacji mojego api… Wchodzę pod adres http://massco.api:56169/docs - a tutaj… Bad request! No ładnie. Także coś nie tak z ASP.NET Core. Niedoróbka - nie nadaje się na produkcję… ale szukam, szukam, szukam… IIS Express. Tutaj włąśnie leży problem. Jeśli chcemy koszystać z custom domain - musimy mieć uprawnienia admina. No cóż. Trzeba było zrobić restart VS z uprawnieniami administratora. Uruchamiam - działa! UF.
No i znów - kolejna próba. Czy tym razem pójdzie? Jest! Działa. Sam kod pisałem poniżej godziny. Pierwsze uruchomienie - dopiero po kolejnej godzinie. Takie to przygody z programowaniem :) Nauka na przyszłość i trzeba iść dalej. Logowania, rejestracja - wszystko działa.
Co dalej?
W kolejnym poście odrobinę o testowaniu kontrollerów. A w kolejnym tygodniu - tworzenie głównej aplikacji z przechowywaniem kontekstu użytkownika. Do usłyszenia!