מדריך 1: httpClient באנגולר

מחבר:
בתאריך:

למדריך זה קיימת גרסה אנגלית מומלצת מאוד Learn to code Angular app with PHP backend: part 1

הדרך המועדפת על Angular לתקשר ב -AJAX עם צד השרת של האפליקציה היא באמצעות המודול HttpClientModule שעתיד להחליף בגרסאות הבאות של אנגולר את ה-HttpModule.

בסדרת המדריכים הזו נלמד כיצד להשתמש במודול אגב בניית אפליקציה מבוססת Angular שכוללת צד משתמש וצד שרת. הקוד שנפתח במדריך הראשון בסדרה מביא את המידע מצד השרת באמצעות GET, ואח"כ מציג אותו באפליקציה האנגולרית.

במדריכים הבאים בסדרה נלמד כיצד להוסיף פריטים, לעדכן, ולמחוק.

כך תראה האפליקציה כשנסיים:

מדריך http אנגולר

לחצו כדי לראות את הקוד בפעולה

 

1. התקנת אנגולר

אם אינכם יודעים כיצד להקים פרויקט Angular, אז כדאי שתקראו את המדריך אודות התקנת אנגולר שפרסמתי באתר.

 

2. הוספת המודול HttpClientModule לפרויקט

כדי שניתן יהיה להשתמש במודול HttpClientModule צריך לייבא אותו למודול השורש של האפליקציה:

src/app/app.module.ts

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

import { AppComponent } from './app.component';
import { CarComponent } from './car/car.component';

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

אחרי הייבוא, ניתן יהיה להשתמש במודול ביחידות השונות של האפליקציה, דוגמת components ו-services.

 

3. הסוג הכללי Car בתוך קובץ קלאס

מכיוון שאנחנו עובדים עם מכוניות אנחנו רוצים להגדיר את הסוג (type), ואת זה נעשה בתוך הקלאס Car, שאותו נוסיף לאפליקציה:

src/app/car.ts

export class Car {
  constructor(
    model: string,
    price: number,
    id?:   number) {}
}

מלבד מחיר ושם המודל, הקלאס מכיל id שהוא שדה רשות, וזו הסיבה לסימן השאלה.

 

4. ריכוז הפונקציות שמתקשרות עם השרת ב- service

אומנם ניתן להתקשר ב-AJAX עם צד השרת ישירות מהקומפננטות אבל זו אינה הדרך המומלצת מכיוון שייתכן שנרצה לעשות שימוש באותם הפונקציות במקומות שונים באפליקציה. לכן, עדיף להפריד את הפונקציות ל-service ייעודי. ובמקרה שלנו, יהיה זה service שייעודו להעביר מידע אודות מכוניות, ושמו בהתאם:

car.service

ניצור את ה-service באמצעות הקלדת הפקודה הבאה ל-CLI:

ng generate service cars --flat --spec=false

הפקודה מורה על יצירת ה-service, וכוללת שני דגלים:

flat -- כי אנחנו רוצים שה-service יכלול רק קבצים ולא יהיה כלוא בתיקייה.

spec=false-- כי אנחנו לא מעוניינים שייווצר קובץ unit test

בתוך הקובץ שנוצר נראה את הקוד הבא:

src/app/car.service.ts

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

@Injectable({
  providedIn: 'root'
})
export class CarService {
}

הדקורטור Injectable מאפשר ל-service להשתתף במערכת הזרקת התלויות (dependency injection). שזה אומר שמצד אחד הוא יכול לקבל תלויות (לדוגמה את המודול HttpClientModule), ומצד שני הוא יכול להיות מוזרק כתלות, לדוגמה לתוך הקומפננטות.

אבל מה זה תלות?

תלות היא המצב שקלאס אחד תלוי בקלאס אחר לתפקודו.

הדרך של אנגולר לנהל את התלויות באפליקציה היא באמצעות Dependency Injection או בקיצור DI, שתפקידו ליצור בשבילנו את הקלאסים שאנחנו זקוקים להם.

הדרך המקובלת בתכנות מונחה עצמים ליצור את הקלאסים שבהם אנחנו תלויים היא באמצעות יצירת התלות כאובייקט בתוך הקונסטרקטור של הקלאס. לדוגמה, אם אנחנו מעוניינים באובייקט של HttpClient, אז הדרך המקובלת היא לעשות את הדבר הבא:

private http = new HttpClient;

אבל זו לא הדרך האנגולרית כי אנגולר מעדיף להשתמש ב- Dependency Injection, שהוא זה שמנהל בשבילנו את האובייקטים שהוא יוצר מהקלאסים.

כדי שה-service יוכל לבצע את תפקידו הוא זקוק להזרקת התלות של HttpClient, ולייבוא של רכיבי קוד שונים שדרושים לניהול התקשורת עם השרת.

src/app/car.service.ts

import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';

import { Observable, throwError } from 'rxjs';
import { map, catchError } from 'rxjs/operators';

import { Car } from './car';

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

הזרקת התלות נעשית ב-constructor, על ידי כך שאנחנו מגדירים את המשתנה הפרטי http כשייך לסוג HttpClient , ואז אנגולר מטפל ביצירת האובייקט של HttpClient בשבילנו באמצעות ה-DI.

היבוא מספריית rxjs מאפשר לנו לעבוד עם observable שהוא המעטפת שאנגולר עוטף באמצעותה את המידע שמגיע מהשרת. השימוש ב-observable במקום ב-call back רגיל כדי לטפל בקוד א-סינכרוני מעניק מספר יתרונות, ובהם: ריבוי אופרטורים שמקלים על הטיפול במידע, כמו גם האפשרות להאזין למידע שהשרת פולט שוב ושוב לאורך זמן.

 

5. הצגת רשימת המכוניות שמגיעה מהשרת

כדי לקבל את רשימת המכוניות שמגיעה מהשרת, נוסיף את הקוד הבא לתוך ה-service:

src/app/car.service.ts

baseUrl = '//localhost/api';
cars: Car[];
                
constructor(private  HttpClient) { }
                
getAll(): Observable<car[]> {
  return this.http.get(`${this.baseUrl}/list`).pipe(
    map((res) => {
      this.cars = res['data'];
      return this.cars;
  }),
  catchError(this.handleError));
}

המתודה getAll מצפה להחזיר את רשימת המכוניות עטופה בתוך Observable:

getAll(): Observable<car[]> {
}

המתודה של HttpClient שבה אנחנו משתמשים כדי להביא את המידע מה-url בצד השרת היא get:

getAll(): Observable<car[]> {
  return this.http.get(`${this.baseUrl}/list`)
}

המידע שחוזר מצד השרת הוא לא רשימת המכוניות, שלה אנו מצפים, אלא מערך המכוניות בתור הערך של מפתח data (מקרה נפוץ בצריכה של api ממקור חיצוני). ככה נראה הקוד שמוחזר מהשרת:

{
  data: 
  [
    {
      id: "1",
      model: "subaru",
      price: "120000"
    },
    {
      id: "2",
      model: "mazda",
      price: "160000"
    }
  ]
}

אבל אותנו מעניין רק החלק הפנימי, ללא המעטפת של ה-data. כדי למצות את המידע, נשתמש באופרטור map של rxjs. וכדי להשתמש באופרטורים אנחנו צריכים לשרשר למתודה-get של httpClient את המתודה pipe של ספריית rxjs.

src/app/car.service.ts

getAll(): Observable<Car[]> {
  return this.http.get(`${this.baseUrl}/list`).pipe(
    map((res) => {
      this.cars = res['data'];
      return this.cars;
    });
}

בתוך map נמצה את מערך המכוניות, נציב אותו במשתנה cars, ואף נחזיר את המערך, בדיוק כפי שהבטחנו בהגדרת הפונקציה.

עד כה ראינו מה עושים כשהמידע שמתקבל הוא תקין, אבל מה קורה במידה וצד השרת מחזיר קוד שגיאה. לדוגמה, קוד 404, לא נמצא, או 500 במקרה של שגיאת שרת? במקרים אילו מטפל catchError:

src/app/car.service.ts

getAll(): Observable<Car[]> {
  return this.http.get(`${this.baseUrl}/list`).pipe(
    map((res) => {
      this.cars = res['data'];
      return this.cars;
  }),
  catchError(this.handleError));
}

שקורא לפונקציה handleError, שזה הקוד שלה:

private handleError(error: HttpErrorResponse) {
  console.log(error);
 
  // return an observable with a user friendly message
  return throwError('Error! something went wrong.');
}

הטיפול בשגיאה כולל זריקת הודעת שגיאה כללית, וגם console.log לצורך דיבוג של השגיאה.

אבל, שימו לב, כדי שה-observable יעבוד, לא מספיק שיש לנו קוד שקורא לשרת, צריך גם קוד שנרשם לתגובה שהוא מחזיר, ואת הקוד הזה נמקם בקומפוננטה.

 

6. הקוד בקומפוננטה שנרשם לתגובת השרת

בקומפוננטה, חייב להיות קוד שנרשם למידע שמוחזר מהשרת באמצעות ה-service, אז בואו נוסיף אותו:

src/app/app.component.ts

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

import { Car } from './car';
import { CarService } from './car.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  cars: Car[];
  error = '';
  success = '';
        
  constructor(private carService: CarService) {
  }
        
  ngOnInit() {
    this.getCars();
  }
        
  getCars(): void {
  }
}

המשתנה cars מכיל את מערך המכוניות שמגיע מהשרת.

בקונסטרקטור נזריק את התלות של ה-carService, כדי שהשירות יהיה זמין לכל הפונקציות של הקונטרולר כבר ברגע שהאובייקט נוצר מהקלאס.

בתוך hook מסוג ngOnInit נקרא לפונקציה getCars.

ngOnInit היא פונקציה מיוחדת של אנגולר שרצה מיד אחרי שהקונסטרקטור מסיים להזריק את התלויות, ולכן זה מקום טוב לקרוא בו להבאת מערך המכוניות.

נוסיף את הקוד שבאמצעותו אנחנו נרשמים להבאת מערך המכוניות או לקבלת שגיאה מצד השרת:

getCars(): void {
  this.carService.getAll().subscribe(
    (res: Car[]) => {
      this.cars = res;
    },
    (err) => {
      this.error = err;
    }
  );
}

ה-call back הראשון בתוך ההרשמה (subscribe) מטפל במקרה שהמידע מתקבל בהצלחה, ומציב את רשימת המכוניות במשתנה cars.

ה- call backהשני מיועד לטיפול בשגיאות בצד השרת.

כשאנחנו נרשמים ל-observable באמצעות subscribe אנחנו מצפים לאחת מ-3 תוצאות:

  • התוצאה הראשונה היא קבלת המידע הדרוש מצד השרת (בדוגמה שלנו, מערך המכוניות),
  • השנייה, שגיאה בצד השרת,
  • והשלישית סיום הבאת המידע.

רוצים להעמיק בהבנת Observable? קראו את המדריך שמסביר את Observable בשפה שווה לכל נפש.

במקרה שלנו, אנחנו מתעניינים רק בשתי האפשרויות הראשונות. זאת מכיוון שהפעולה לא צפויה להסתיים כי הרשימה יכולה להתעדכן בכל זמן נתון בפריטים שנוסיף באמצעות הטופס שאותו ניצור בהמשך המדריך.

ועכשיו כל מה שנשאר לנו הוא להציג את ה-html עם רשימת המכוניות, ועם הודעות ההצלחה והשגיאה.

<div *ngIf="error">{{error}}</div>
<div *ngIf="success">{{success}}</div>
    
<div id="theList">
  <h2>The list</h2>
  <ul>
    <li *ngFor="let item of cars">{{item.model}} | {{item.price}} </li>
  </ul>
</div>

הרשימה מודפסת למסך בתוך לולאת ngFor ממערך cars שאותו נקבל מהשרת.

 

7. צד השרת

את הקוד בצד השרת כתבתי כסקריפט PHP שיושב על מסד נתונים mySQL . הכי פשוט שאפשר. העקרונות נכונים לכל שפה.

להלן קוד ה-mysql לטבלה cars שמשמשת לאחסון המידע באפליקציה:

CREATE TABLE IF NOT EXISTS `cars` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `model` varchar(255) NOT NULL DEFAULT '',
  `price` int (10) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

הקובץ .htaccess הוא קובץ הקונפיגורציה של שרת ה- Apache, ובו נגדיר:

  1. כלל להסרת סיומת ה-php משמות הקבצים
  2. headers שיאפשרו לנו להעביר מידע ולבצע פעולות על השרת למרות שהחלק האנגולרי של האפליקציה יושב על שרת נפרד בכתובת נפרדת.

.htaccess

# Remove the php extension from the filename
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^([^\.]+)$ $1.php [NC,L]

# Set the headers for the restful api
Header always set Access-Control-Allow-Origin //localhost:4200
Header always set Access-Control-Max-Age "1000"
Header always set Access-Control-Allow-Headers "X-Requested-With, Content-Type, Origin, Authorization, Accept, Client-Security-Token, Accept-Encoding"
Header always set Access-Control-Allow-Methods "POST, GET, OPTIONS, DELETE, PUT"

ה-header :

Header always set Access-Control-Allow-Origin "//localhost:4200"

מורה לשרת לאפשר גישה למידע ולפעולות מהכתובת שעליה רצה האפליקציה האנגולרית במחשב האישי שלי.

יתר ה-headers מתירים החלפת מידע בין האפליקציה האנגולרית וצד השרת באמצעות המתודות של פרוטוקול HTTP:

POST לשמירת מידע חדש בצד השרת, דוגמת מכונית חדשה.
GET לקבל מידע אודות פריט בודד או רשימת פריטים (מכונית או מערך מכוניות).
PUT לעריכת מידע שכבר נשמר על השרת.
DELETE למחיקה.

הקובץ connect.php מכיל קוד להתקשרות עם מסד הנתונים.

connect.php

<?php

// db credentials
define('DB_HOST', 'localhost');
define('DB_USER', 'root');
define('DB_PASS', '');
define('DB_NAME', 'angular_db');

// Connect with the database.
function connect()
{
  $connect = mysqli_connect(DB_HOST ,DB_USER ,DB_PASS ,DB_NAME);

  if (mysqli_connect_errno($connect)) {
    die("Failed to connect:" . mysqli_connect_error());
  }

  mysqli_set_charset($connect, "utf8");

  return $connect;
}

$con = connect();

צד השרת מכיל את הקובץ list.php שמקבל בקשה ב GET מצד האפליקציה האנגולרית, ומחזיר את המידע שאותו הוא שולף ממסד הנתונים.

list.php

<?php
/**
 * Returns the list of cars.
 */
require 'connect.php';
    
$cars = [];
$sql = "SELECT id, model, price FROM cars";

if($result = mysqli_query($con,$sql))
{
  $cr = 0;
  while($row = mysqli_fetch_assoc($result))
  {
    $cars[$cr]['id']    = $row['id'];
    $cars[$cr]['model'] = $row['model'];
    $cars[$cr]['price'] = $row['price'];
    $cr++;
  }
    
  echo json_encode(['data'=>$cars]);
}
else
{
  http_response_code(404);
}

במדריך הבא, נלמד לשלוח מידע ב- POST מהאפליקציה האנגולרית אל צד השרת.

 

אהבתם? לא אהבתם? דרגו!

0 הצבעות, ממוצע 0 מתוך 5 כוכבים

 

הוסף תגובה חדשה

 

= 7 + 4