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

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

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

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

להורדת קוד האנגולר אותו נפתח במדריך

בסדרת המדריכים הזו נלמד כיצד להשתמש במודול לבניית אפליקצית 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';

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

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

 

3. הגדרת הסוג הכללי interface

interface של TypeScript מחייב כל קוד שמיישם אותו לכתוב מתודות או שדות מסוימים.

אפשר להסתבך בהסברים אבל יותר פשוט לכתוב את הקוד. נוסיף את ה-interface שמגדיר את הסוג מכונית עבור האפליקציה שלנו בתוך קובץ חדש שנוסיף car.ts:

src/app/car.ts

export interface Car {
  model: string;
  price: number;
  id?: number;
}

בהמשך, בכל פעם שנזדקק לסוג Car נהיה מחויבים לספק מידע אודות השדות model ו-price. את השדה id, לעומת זאת, אנחנו לא חייבים לספק כי הוא אופציונלי כפי שמעיד סימן השאלה לימינו.

 

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

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

car.service

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

ng generate service car --flat

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

בתוך קובץ ה- service נמצא את הקוד הבא:

src/app/car.service.ts

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

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

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

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

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

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

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

private http = new HttpClient;

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

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

src/app/car.service.ts

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

import { map } from 'rxjs/operators';

import { Car } from './car';

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

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

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

 

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

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

src/app/car.service.ts

baseUrl = 'http ://localhost/api/';
                
constructor(private http : HttpClient) { }
                
getAll() {
    return this.http.get(`${this.baseUrl}list`).pipe(
      map((res: any) => {
        return res['data'];
      })
    );
}
  • המתודה getAll מצפה להחזיר את רשימת המכוניות עטופה בתוך Observable.

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

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

    {
      data: 
      [
        {
          id: "1",
          model: "subaru",
          price: "120000"
        },
        {
          id: "2",
          model: "mazda",
          price: "160000"
        }
      ]
    }
  • אבל אותנו מעניין רק החלק הפנימי, ללא המעטפת של ה-data. כדי למצות את המידע, נשתמש באופרטור map של rxjs. וכדי להשתמש באופרטורים אנחנו צריכים לשרשר למתודה-get של httpClient את המתודה pipe של ספריית rxjs.

  • בתוך map נמצה את מערך המכוניות כדי להחזיר אותו.

כך נראה הקוד המלא של ה-service:

src/app/car.service.ts

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

import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root',
})
export class CarService {
  baseUrl = 'http ://localhost/api/';

  constructor(private http : HttpClient) {}

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

 

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 {
    // here we'll write the method code
  }
}
  • המשתנה cars מכיל את מערך המכוניות שמגיע מהשרת.
  • בקונסטרקטור נזריק את התלות של ה-carService, כדי שהשירות יהיה זמין לכל הפונקציות של הקונטרולר כבר ברגע שהאובייקט נוצר מהקלאס.
  • בתוך hook מסוג ngOnInit נקרא לפונקציה getCars.
  • ngOnInit היא פונקציה מיוחדת של אנגולר שרצה מיד אחרי שהקונסטרקטור מסיים להזריק את התלויות, ולכן זה מקום טוב לקרוא בו להבאת מערך המכוניות.

לא להכנס לסרטים אם האפליקציה לא מתקמפלת כי מיד נוסיף את הנתונים החסרים.

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

getCars(): void {
    this.carService
    .getAll()
    .subscribe(
      (data: Car[]) => {
        this.cars = data
        this.success = 'Operation success'
      },
      (err) => {
        console.log(err)
        this.error = err.message;
      }
    );
}

ה-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 car of cars">{{car.price}} {{car.model}}</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 כוכבים

 

 

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

 

= 6 + 3

תמונת המגיב

Roxi בתאריך: 09.07.2019

אני אוהבת את האתר שלך. הוא עוזר לי מאוד.

תמונת המגיב

אביגיל בתאריך: 21.01.2020

מושלם ממש עזר לי

תמונת המגיב

מנור בתאריך: 13.02.2020

מצוין

תמונת המגיב

מנור בתאריך: 25.02.2020

כאשר אני מנסה לייבא את HttpClientModule הוא רושם לי שגיאה האם יש לזה הסבר?