تطوير تطبيقات الويب باستخدام Angular


الدرس: إنشاء تطبيق كامل مع Angular و Firebase


الصفحة السابقة
بالنسبة لهذا الجزء ، ستقوم بإنشاء تطبيق جديد وتطبق المعرفة التي تعلمتها طوال الدورة التدريبيةAngular، بالإضافة إلى بعض الميزات التي لم تصادفها بعد. ستقوم بإنشاء تطبيق بسيط يسرد الكتب التي لديك في المنزل ، في مكتبتك. يمكنك إضافة صورة لكل كتاب. يجب المصادقة على المستخدم لاستخدام التطبيق.
على الرغم من شعبيتها ، اخترت عدم دمج AngularFire في هذه الدورة. إذا كنت تريد معرفة المزيد ، يمكنك العثور على مزيد من المعلومات على صفحة GitHub من AngularFire  . ستستخدم واجهة برمجة تطبيقات جافا سكريبت التي يتم توفيرها مباشرةً بواسطة Firebase .

فكر في هيكل التطبيق


خصص بعض الوقت للتفكير في إنشاء التطبيق. ما المكونات التي ستحتاجها؟ الخدمات ؟ نماذج البيانات؟
يتطلب التطبيق المصادقة. لذلك سيتطلب مكونًا لإنشاء مستخدم جديد ، وآخر للمصادقة ، مع خدمة إدارة التفاعلات مع الواجهة الخلفية.
يمكن عرض الكتب كقائمة كاملة ، ثم بشكل فردي. يجب أيضًا أن تكون قادرًا على إضافة الكتب وحذفها. لذلك ، سيكون هناك حاجة إلى مكون للقائمة الكاملة ، وآخر للعرض الفردي وآخر يتضمن نموذجًا للإنشاء / التعديل. ستكون هناك حاجة إلى خدمة لإدارة جميع الوظائف المرتبطة بهذه المكونات ، بما في ذلك التفاعلات مع الخادم.
ستقوم أيضًا بإنشاء مكون منفصل لشريط التنقل لدمج منطق منفصل.
بالنسبة لنماذج البيانات ، سيكون هناك نموذج للكتب ، مع العنوان واسم المؤلف والصورة فقط ، والذي سيكون اختياريًا.
سيضيف أيضًا التوجيه إلى هذا التطبيق ، مما يسمح بالوصول إلى أجزاء مختلفة ، مع حماية لجميع المسارات باستثناء المصادقة ، مما يمنع المستخدمين غير المصادقين من الوصول إلى المكتبة.
تعال ، دعنا نذهب!

هيكلة التطبيق


لهذا التطبيق ، أنصحك باستخدام CLI لإنشاء المكونات. سيكون هيكل الشجرة على النحو التالي:

ng g c auth/signup
ng g c auth/signin
ng g c book-list
ng g c book-list/single-book
ng g c book-list/book-form
ng g c header
ng g s services/auth
ng g s services/books
ng g s services/auth-guard
لا يتم وضع الخدمات وبالتالي إنشاؤها تلقائيا في مجموعة   providers  من AppModule ، لذلك إضافتها الآن. بينما كنت تعمل على   AppModule إضافة أيضا   FormsModule ،    ReactiveFormsModule   و   HttpClientModule  :

imports: [
    BrowserModule,
    FormsModule,
    ReactiveFormsModule,
    HttpClientModule
  ],
providers: [AuthService, BooksService, AuthGuardService],
لا تنس أن تضيف عمليات الاستيراد في أعلى الملف!
دمج التوجيه (routing) بدون حراسة (guard) الآن لتتمكن من الوصول إلى جميع أقسام التطبيق أثناء التطوير:

const appRoutes: Routes = [
  { path: 'auth/signup', component: SignupComponent },
  { path: 'auth/signin', component: SigninComponent },
  { path: 'books', component: BookListComponent },
  { path: 'books/new', component: BookFormComponent },
  { path: 'books/view/:id', component: SingleBookComponent }
];
imports: [
    BrowserModule,
    FormsModule,
    ReactiveFormsModule,
    HttpClientModule,
    RouterModule.forRoot(appRoutes)
],
قم أيضًا بإنشاء مجلد يسمى models وإنشاء الملف هناك book.model.ts :

export class Book {
  photo: string;
  synopsis: string;
  constructor(public title: string, public author: string) {
  }
}
قبل البدء ng serve ، استخدم NPM لإضافة Bootstrap إلى مشروعك وإضافته إلى أنماط الصفيف من .angular-cli.json :

npm install bootstrap@3.3.7 –save


"styles": [
        "../node_modules/bootstrap/dist/css/bootstrap.css",
        "styles.css"
  ],
أخيرًا ، قم بالتحضير من HeaderComponent خلال قائمة تنقل مع routerLink AppComponent ودمجها مع router-outlet :

<nav class="navbar navbar-default">
  <div class="container-fluid">
    <ul class="nav navbar-nav">
      <li routerLinkActive="active">
        <a routerLink="books">Livres</a>
      </li>
    </ul>
    <ul class="nav navbar-nav navbar-right">
      <li routerLinkActive="active">
        <a routerLink="auth/signup">Créer un compte</a>
      </li>
      <li routerLinkActive="active">
        <a routerLink="auth/signin">Connexion</a>
      </li>
    </ul>
  </div>
</nav>
<app-header></app-header>
<div class="container">
  <router-outlet></router-outlet>
</div>
الهيكل العام للتطبيق جاهز الآن!

دمجFirebaseفي تطبيقك


أولاً ، قم بتثبيت Firebase باستخدام NPM :

npm installFirebase–save
لهذا التطبيق ، ستنشئ مشروعًا جديدًا على Firebase . بمجرد إنشاء التطبيق ، تقدم لك وحدة تحكم Firebase الخيار التالي (ضمن قسم "نظرة عامة"):
Angular web site
اختر "إضافة Firebase إلى تطبيق الويب الخاص بك" وانسخ والصق الاعداد في المنشئ   AppComponent  (عن طريق إضافة   import * asFirebasefrom 'firebase' ;  الجزء العلوي من الملف ، وإتاحة الطريقة   initializeApp()  ) : 

import { Component } from '@angular/core';
import * asFirebasefrom 'firebase';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  constructor() {
    const config = {
      apiKey: 'AIzaSyCwfa_fKNCVrDMR1E88S79mpQP-6qertew4',
      authDomain: 'bookshelves-3d570.firebaseapp.com',
      databaseURL: 'https://bookshelves-3d570.firebaseio.com',
      projectId: 'bookshelves-3d570',
      storageBucket: 'bookshelves-3d570.appspot.com',
      messagingSenderId: '6634573823'
    };
    firebase.initializeApp(config);
  }
}
تم ربط تطبيقكAngularالآن بمشروع Firebase ، ويمكنك الآن دمج جميع الخدمات التي ستحتاج إليها.

المصادقة


سيستخدم تطبيقك المصادقة عن طريق عنوان البريد الإلكتروني وكلمة المرور التي يقدمها Firebase . للقيام بذلك ، يجب عليك أولاً تنشيطه في وحدة تحكم Firebase :
Angular web site
Angular web site
تستخدم مصادقةFirebaseنظامًا مميزًا: يتم تخزين رمز مصادقة مميز في المتصفح ، ويتم إرساله مع كل طلب يتطلب المصادقة.
في   AuthService ، ستقوم بإنشاء ثلاث طرق:
  • طريقة لإنشاء مستخدم جديد ؛
  • طريقة لربط مستخدم موجود ؛
  • طريقة لتسجيل خروج المستخدم.
نظرًا لأن عمليات الإنشاء والاتصال والانفصال غير متزامنة ، أي أنه ليس لديهم نتيجة فورية ، فإن الطرق التي ستنشئها لإدارتها ستعيد Promise ، مما سيسمح أيضًا معالجة حالات الخطأ.
استيرادFirebaseإلى   AuthService  : 

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

import * asFirebasefrom 'firebase';

@
Injectable()
export class AuthService {
ثم قم بإنشاء طريقة   createNewUser()  لإنشاء مستخدم جديد ، والذي سيأخذ كمُدخل عنوان بريد إلكتروني وكلمة مرور ، والذي سيعيد Promise الذي سيحل إذا نجح الإنشاء ، وسيتم رفضه برسالة الخطأ إذا لم ينجح :

createNewUser(email: string, password: string) {
    return new Promise(
      (resolve, reject) => {
        firebase.auth().createUserWithEmailAndPassword(email, password).then(
          () => {
            resolve();
          },
          (error) => {
            reject(error);
          }
        );
      }
    );
}
يمكن العثور على جميع الطرق المتعلقة بمصادقةFirebaseفي   firebase.auth() .
قم أيضًا بإنشاء   signInUser() طريقة مشابهة جدًا ، والتي ستهتم بتوصيل مستخدم موجود بالفعل:

signInUser(email: string, password: string) {
    return new Promise(
      (resolve, reject) => {
        firebase.auth().signInWithEmailAndPassword(email, password).then(
          () => {
            resolve();
          },
          (error) => {
            reject(error);
          }
        );
      }
    );
}
إنشاء طريقة بسيطة signOutUser() :

signOutUser() {
    firebase.auth().signOut();
}
لذا ، لديك الوظائف الثلاث التي تحتاجها لدمج المصادقة في التطبيق!
يمكنك إنشاء   SignupComponent  و   SigninComponent دمج المصادقة في   HeaderComponent  أجل إظهار الروابط الصحيحة وتنفيذها   AuthGuard  لحماية الطريق   /books  بكل ما فيها من طرق فرعية.
ابدأ   SignupComponent  حتى تتمكن من تسجيل مستخدم:

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AuthService } from '../../services/auth.service';
import { Router } from '@angular/router';

@Component({
  selector: 'app-signup',
  templateUrl: './signup.component.html',
  styleUrls: ['./signup.component.css']
})
export class SignupComponent implements OnInit {

  signupForm: FormGroup;
  errorMessage: string;

  constructor(private formBuilder: FormBuilder,
              private authService: AuthService,
              private router: Router) { }

  ngOnInit() {
    this.initForm();
  }

  initForm() {
    this.signupForm = this.formBuilder.group({
      email: ['', [Validators.required, Validators.email]],
      password: ['', [Validators.required, Validators.pattern(/[0-9a-zA-Z]{6,}/)]]
    });
  }

  onSubmit() {
    const email = this.signupForm.get('email').value;
    const password = this.signupForm.get('password').value;
    
    this.authService.createNewUser(email, password).then(
      () => {
        this.router.navigate(['/books']);
      },
      (error) => {
        this.errorMessage = error;
      }
    );
  }
}
في هذا المكون:
  • يمكنك إنشاء النموذج باستخدام الطريقة التفاعلية
    • الحقلان ، email و password ، مطلوبان - الحقل email المستخدم Validators.email لفرض سلسلة تحت تنسيق عنوان البريد الإلكتروني ؛ password يستخدم المجال Validators.pattern لفرض 6 أحرف أبجدية رقمية على الأقل ، وهو ما يتوافق مع الحد الأدنى المطلوب منFirebase؛
  • يمكنك إدارة إرسال النموذج ، وإرسال القيم التي أدخلها المستخدم إلى الطريقة createNewUser()
    • إذا كان التصميم الإبداعي يعمل على إعادة توجيه المستخدم إلى /books ؛
    • إذا لم يفلح ذلك ، يمكنك عرض رسالة الخطأ التي أرجعتها Firebase.
أدناه ، ستجد القالب المقابل:

<div class="row">
  <div class="col-sm-8 col-sm-offset-2">
    <h2>Créer un compte</h2>
    <form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
      <div class="form-group">
        <label for="email">Adresse mail</label>
        <input type="text"
               id="email"
               class="form-control"
               formControlName="email">
      </div>
      <div class="form-group">
        <label for="password">Mot de passe</label>
        <input type="password"
               id="password"
               class="form-control"
               formControlName="password">
      </div>
      <button class="btn btn-primary"
              type="submit"
              [disabled]="signupForm.invalid">Créer mon compte</button>
    </form>
    <p class="text-danger">{ { errorMessage }}</p>
  </div>
</div>
إنه نموذج وفقًا للطريقة التفاعلية كما رأيت في فصل سابق. بالإضافة إلى ذلك ، هناك فقرة تحتوي على رسالة الخطأ المحتملة التي قدمها Firebase.
يمكنك إنشاء قالب مطابق تقريبًا   SignInComponent  لاتصال مستخدم موجود بالفعل. ببساطة إعادة تسمية   signupForm  في   signinForm  واستدعاء الأسلوب   signInUser()  بدلا   createNewUser()  .
بعد ذلك ، ستقوم بالتعديل   HeaderComponent  لعرض روابط الاتصال وإنشاء المستخدم وفصل الاتصال حسب المحتوى:

import { Component, OnInit } from '@angular/core';
import { AuthService } from '../services/auth.service';
import * asFirebasefrom 'firebase';

@Component({
  selector: 'app-header',
  templateUrl: './header.component.html',
  styleUrls: ['./header.component.css']
})
export class HeaderComponent implements OnInit {

  isAuth: boolean;

  constructor(private authService: AuthService) { }

  ngOnInit() {
    firebase.auth().onAuthStateChanged(
      (user) => {
        if(user) {
          this.isAuth = true;
        } else {
          this.isAuth = false;
        }
      }
    );
  }

  onSignOut() {
    this.authService.signOutUser();
  }

}
<nav class="navbar navbar-default">
  <div class="container-fluid">
    <ul class="nav navbar-nav">
      <li routerLinkActive="active">
        <a routerLink="books">Livres</a>
      </li>
    </ul>
    <ul class="nav navbar-nav navbar-right">
      <li routerLinkActive="active" *ngIf="!isAuth">
        <a routerLink="auth/signup">Créer un compte</a>
      </li>
      <li routerLinkActive="active" *ngIf="!isAuth">
        <a routerLink="auth/signin">Connexion</a>
      </li>
      <li>
        <a (click)="onSignOut()"
           style="cursor:pointer"
           *ngIf="isAuth">Déconnexion</a>
      </li>
    </ul>
  </div>
</nav>
هنا ، يمكنك استخدام   onAuthStateChanged() ، مما يسمح لك بمراقبة حالة مصادقة المستخدم: في كل مرة تتغير فيها الحالة ، يتم تنفيذ الوظيفة التي تمر بها كوسيطة. إذا تم مصادقة المستخدم بشكل صحيح ، فإنه   onAuthStateChanged()  يتلقى الكائن من النوع   firebase.User  المطابق للمستخدم. يمكنك بالتالي بناء قيمة المتغير المحلي   isAuth  وفقًا لحالة المصادقة للمستخدم ، وعرض الروابط المقابلة لهذه الحالة.
كل ما عليك فعله هو إنشاء   AuthGuardService  وتطبيقه على الطرق المعنية. نظرًا لأن التحقق من المصادقة غير متزامن ، فستعرض الخدمة Promise :

import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import * asFirebasefrom 'firebase';

@Injectable()
export class AuthGuardService implements CanActivate {

  constructor(private router: Router) { }

  canActivate(): Observable<boolean> | Promise<boolean> | boolean {
    return new Promise(
      (resolve, reject) => {
        firebase.auth().onAuthStateChanged(
          (user) => {
            if(user) {
              resolve(true);
            } else {
              this.router.navigate(['/auth', 'signin']);
              resolve(false);
            }
          }
        );
      }
    );
  }
}
const appRoutes: Routes = [
  { path: 'auth/signup', component: SignupComponent },
  { path: 'auth/signin', component: SigninComponent },
  { path: 'books', canActivate: [AuthGuardService], component: BookListComponent },
  { path: 'books/new', canActivate: [AuthGuardService], component: BookFormComponent },
  { path: 'books/view/:id', canActivate: [AuthGuardService], component: SingleBookComponent }
];
آه ، لكن ماذا نسينا؟ التوجيه لا يأخذ في الاعتبار المسار الفارغ ولا حرف بدل المسار! أضف هذه المسارات الآن لتجنب أي أخطاء:

const appRoutes: Routes = [
  { path: 'auth/signup', component: SignupComponent },
  { path: 'auth/signin', component: SigninComponent },
  { path: 'books', canActivate: [AuthGuardService], component: BookListComponent },
  { path: 'books/new', canActivate: [AuthGuardService], component: BookFormComponent },
  { path: 'books/view/:id', canActivate: [AuthGuardService], component: SingleBookComponent },
  { path: '', redirectTo: 'books', pathMatch: 'full' },
  { path: '**', redirectTo: 'books' }
];
وبالتالي ، يشتمل تطبيقك على نظام مصادقة كامل ، يسمح بالتسجيل والاتصال / فصل المستخدمين ، والذي يحمي المسارات المعنية. يمكنك الآن إضافة الوظائف إلى تطبيقك مع العلم أن الوصول إلى قاعدة البيانات والتخزين ، والذي يتطلب المصادقة ، سيعمل بشكل صحيح.

قاعدة البيانات


في هذا الفصل ، ستنشئ وظائف التطبيق: إنشاء وعرض وحذف الكتب ، وكلها مرتبطة مباشرة بقاعدة بياناتFirebase.
لإنشاء   BooksService  :
  • سيكون لديك صفيف محلي وموضوع   books  لإرساله ؛
  • سيكون لديك طرق:
    • لحفظ قائمة الكتب على الخادم ،
    • لاسترداد قائمة الكتب من الخادم ،
    • لاسترداد كتاب واحد ،
    • لإنشاء كتاب جديد ،
    • لحذف كتاب موجود.
بالنسبة للخطوة الأولى ، لا شيء جديد (دون نسيان استيراد الكتاب والموضوع):

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { Book } from '../models/book.model';

@Injectable()
export class BooksService {

  books: Book[] = [];
  booksSubject = new Subject<Book[]>();

  emitBooks() {
    this.booksSubject.next(this.books);
  }
}
بعد ذلك ، ستستخدم طريقة يوفرهاFirebaseلحفظ القائمة في عقدة قاعدة بيانات - الطريقة set() :

saveBooks() {
    firebase.database().ref('/books').set(this.books);
}
تقوم الطريقة   ref()  بإرجاع مرجع إلى العقدة المطلوبة لقاعدة البيانات ،   set()   وتعمل بشكل أو بآخر مثل   put() في   HTTP : فهي تكتب البيانات وتحل محلها في العقدة المحددة.
الآن بعد أن يمكنك حفظ القائمة ، ستنشئ طرقًا لاسترداد القائمة الكاملة للكتب واسترداد كتاب واحد ، باستخدام الوظيفتين اللتين يوفرهما Firebase:

getBooks() {
    firebase.database().ref('/books')
      .on('value', (data: DataSnapshot) => {
          this.books = data.val() ? data.val() : [];
          this.emitBooks();
        }
      );
  }

  getSingleBook(id: number) {
    return new Promise(
      (resolve, reject) => {
        firebase.database().ref('/books/' + id).once('value').then(
          (data: DataSnapshot) => {
            resolve(data.val());
          }, (error) => {
            reject(error);
          }
        );
      }
    );
  }
بالنسبة لـ   getBooks() ، يمكنك استخدام الطريقة   on()  .  تطلب الوسيطة الأولى  'value'  منFirebaseتنفيذ رد الاتصال لكل تغيير في القيمة المسجلة عند نقطة النهاية المختارة: وهذا يعني أنه إذا قمت بتغيير شيء ما من جهاز ، فسيتم تحديث القائمة تلقائيًا على جميع الأجهزة المتصلة. أضف مُنشئ إلى الخدمة للاتصال به   getBooks()  عند بدء التطبيق:

constructor() {
    this.getBooks();
}
الوسيطة الثانية هي وظيفة رد الاتصال ، والتي تتلقى هنا   DataSnapshot  : كائن يتوافق مع العقدة المطلوبة ، التي تضم العديد من الأعضاء والأساليب (يجب استيرادها   DataSnapshot  من   firebase.database.DataSnapshot  ) . الطريقة التي تهتم بها هنا val() ، هي   ببساطة إرجاع قيمة البيانات. يأخذ رد الاتصال الخاص بك أيضًا في الاعتبار الحالة التي لا يقوم الخادم فيها بإرجاع أي شيء لتجنب الأخطاء المحتملة.
  تسترجع الوظيفة getSingleBook()  كتابًا وفقًا لمعرفه ، وهو ببساطة فهرسه هنا في الصفيف المحفوظ. يمكنك استخدام   once() الأمر الذي يجعل طلب واحد فقط للبيانات. فجأة ، لا تأخذ وظيفة رد الاتصال كوسيطة ولكنها تُرجع وعدًا ، مما يسمح باستخدام   .then()  إرجاع البيانات المستلمة.
بالنسبة لـ BooksService ، كل ما عليك فعله هو إنشاء طرق لإنشاء كتاب جديد وحذف كتاب موجود:

createNewBook(newBook: Book) {
    this.books.push(newBook);
    this.saveBooks();
    this.emitBooks();
  }

  removeBook(book: Book) {
    const bookIndexToRemove = this.books.findIndex(
      (bookEl) => {
        if(bookEl === book) {
          return true;
        }
      }
    );
    this.books.splice(bookIndexToRemove, 1);
    this.saveBooks();
    this.emitBooks();
  }
ثم ستقوم بإنشاء BookListComponent ما يلي:
  • يشترك في موضوع الخدمة ويبدأ إرسالها الأول ؛
  • يعرض قائمة الكتب ، حيث يمكن النقر على كل كتاب لعرض الصفحة   SingleBookComponent ؛
  • يسمح لك بحذف كل كتاب باستخدام   removeBook() ؛
  • يسمح بالانتقال إلى   BookFormComponent  إنشاء كتاب جديد.  

import { Component, OnDestroy, OnInit } from '@angular/core';
import { BooksService } from '../services/books.service';
import { Book } from '../models/book.model';
import { Subscription } from 'rxjs/Subscription';
import { Router } from '@angular/router';

@Component({
  selector: 'app-book-list',
  templateUrl: './book-list.component.html',
  styleUrls: ['./book-list.component.css']
})
export class BookListComponent implements OnInit, OnDestroy {

  books: Book[];
  booksSubscription: Subscription;

  constructor(private booksService: BooksService, private router: Router) {}

  ngOnInit() {
    this.booksSubscription = this.booksService.booksSubject.subscribe(
      (books: Book[]) => {
        this.books = books;
      }
    );
    this.booksService.emitBooks();
  }

  onNewBook() {
    this.router.navigate(['/books', 'new']);
  }

  onDeleteBook(book: Book) {
    this.booksService.removeBook(book);
  }

  onViewBook(id: number) {
    this.router.navigate(['/books', 'view', id]);
  }
  
  ngOnDestroy() {
    this.booksSubscription.unsubscribe();
  }
}
<div class="row">
  <div class="col-xs-12">
    <h2>Vos livres</h2>
    <div class="list-group">
      <button
        class="list-group-item"
        *ngFor="let book of books; let i = index"
        (click)="onViewBook(i)">
        <h3 class="list-group-item-heading">
        { { book.title }}
          <button class="btn btn-default pull-right" (click)="onDeleteBook(book)">
            <span class="glyphicon glyphicon-minus"></span>
          </button>
        </h3>
        <p class="list-group-item-text">{ { book.author }}</p>
      </button>
    </div>
    <button class="btn btn-primary" (click)="onNewBook()">Nouveau livre</button>
  </div>
</div>
لا يوجد شيء جديد هنا ، لذا انتقل بسرعة إلى SingleBookComponent :

import { Component, OnInit } from '@angular/core';
import { Book } from '../../models/book.model';
import { ActivatedRoute, Router } from '@angular/router';
import { BooksService } from '../../services/books.service';

@Component({
  selector: 'app-single-book',
  templateUrl: './single-book.component.html',
  styleUrls: ['./single-book.component.css']
})
export class SingleBookComponent implements OnInit {

  book: Book;

  constructor(private route: ActivatedRoute, private booksService: BooksService,
              private router: Router) {}

  ngOnInit() {
    this.book = new Book('', '');
    const id = this.route.snapshot.params['id'];
    this.booksService.getSingleBook(+id).then(
      (book: Book) => {
        this.book = book;
      }
    );
  }

  onBack() {
    this.router.navigate(['/books']);
  }
}
يسترجع المكون الكتاب المطلوب بمعرفه بفضل getSingleBook() ويعرضه في القالب التالي:

<div class="row">
  <div class="col-xs-12">
    <h1>{ { book.title }}</h1>
    <h3>{ { book.author }}</h3>
    <p>{ { book.synopsis }}</p>
    <button class="btn btn-default" (click)="onBack()">Retour</button>
  </div>
</div>
يبقى فقط لإنشاء BookFormComponent ، والذي يتضمن نموذجًا باستخدام الطريقة التفاعلية ويسجل البيانات المستلمة بفضل createNewBook() :

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Book } from '../../models/book.model';
import { BooksService } from '../../services/books.service';
import { Router } from '@angular/router';

@Component({
  selector: 'app-book-form',
  templateUrl: './book-form.component.html',
  styleUrls: ['./book-form.component.css']
})
export class BookFormComponent implements OnInit {

  bookForm: FormGroup;

  constructor(private formBuilder: FormBuilder, private booksService: BooksService,
              private router: Router) { }
              
  ngOnInit() {
    this.initForm();
  }
  
  initForm() {
    this.bookForm = this.formBuilder.group({
      title: ['', Validators.required],
      author: ['', Validators.required],
      synopsis: ''
    });
  }
  
  onSaveBook() {
    const title = this.bookForm.get('title').value;
    const author = this.bookForm.get('author').value;
    const synopsis = this.bookForm.get('synopsis').value;
    const newBook = new Book(title, author);
    newBook.synopsis = synopsis;
    this.booksService.createNewBook(newBook);
    this.router.navigate(['/books']);
  }
}


<div class="row">
  <div class="col-sm-8 col-sm-offset-2">
    <h2>Enregistrer un nouveau livre</h2>
    <form [formGroup]="bookForm" (ngSubmit)="onSaveBook()">
      <div class="form-group">
        <label for="title">Titre</label>
        <input type="text" id="title"
               class="form-control" formControlName="title">
      </div>
      <div class="form-group">
        <label for="author">Auteur</label>
        <input type="text" id="author"
               class="form-control" formControlName="author">
      </div>
      <div class="form-group">
        <label for="synopsis">Synopsis</label>
        <textarea id="synopsis"
                  class="form-control" formControlName="synopsis">
        </textarea>
      </div>
      <button class="btn btn-success" [disabled]="bookForm.invalid"
              type="submit">Enregistrer
      </button>

    </form>
  </div>
</div>
وهذا كل شيء ، يعمل التطبيق الخاص بك! يحفظ ويقرأ قائمة كتبك على الواجهة الخلفية لـFirebase، مما يجعل تشغيلها ديناميكيًا تمامًا!
إذا كنت تريد أن ترى كيف تعمل قاعدة البيانات في الوقت الحقيقي ، افتح نافذة ثانية وأضف كتابًا جديدًا إليها: ستراه يظهر في النافذة الأولى على الفور تقريبًا!
لإكمال هذا التطبيق ، ستضيف الوظيفة التي تتيح لك حفظ صورة لكل كتاب باستخدامFirebaseStorage API .

التخزين


في هذا الفصل الأخير ، ستتعلم كيفية استخدامFirebaseStorage API للسماح للمستخدم بإضافة صورة للكتاب وعرضها   SingleBookComponent  وحذفها إذا تم حذف الكتاب ، حتى لا يغادر الصور غير المستخدمة على الخادم.
أولاً ، ستضيف طريقة   BooksService  تتيح لك تحميل صورة:

uploadFile(file: File) {
    return new Promise(
      (resolve, reject) => {
        const almostUniqueFileName = Date.now().toString();
        const upload = firebase.storage().ref()
          .child('images/' + almostUniqueFileName + file.name).put(file);
        upload.on(firebase.storage.TaskEvent.STATE_CHANGED,
          () => {
            console.log('Chargement…');
          },
          (error) => {
            console.log('Erreur de chargement ! : ' + error);
            reject();
          },
          () => {
            resolve(upload.snapshot.ref.getDownloadURL());
          }
        );
      }
    );
}
حلل هذه الطريقة:
  • تستغرق عملية تنزيل ملف بعض الوقت ، لذلك تقوم بإنشاء طريقة غير متزامنة تقوم بإرجاع Promise ؛
  • تأخذ الطريقة كوسيطة ملف من النوع File ؛
  • لإنشاء اسم فريد للملف (وبالتالي تجنب الكتابة فوق ملف يحمل نفس الاسم الذي يحاول المستخدم تحميله) ، يمكنك إنشاء سلسلة منه   Date.now() ، والتي تعطي عدد المللي ثانية التي مرت منذ 1 يناير 1970 ؛
  • ثم تقوم بإنشاء مهمة تحميل   upload  :
    • firebase.storage().ref()  إرجاع مرجع إلى جذر مجموعةFirebase،
    • تقوم الطريقة   child()  بإرجاع مرجع إلى المجلد الفرعي   images  وإلى ملف جديد اسمه المعرف الفريد + الاسم الأصلي للملف (مما يسمح بالاحتفاظ بالتنسيق الأصلي أيضًا) ،
  • ثم تستخدم طريقة on() المهمة upload لتتبع حالتها ، وتمرير ثلاث وظائف:
    • يتم تشغيل الأولى في كل مرة يتم فيها إرسال البيانات إلى الخادم ،
    • يتم تشغيل الثانية إذا أعاد الخادم خطأ ،
    • يتم تشغيل الثالثة عند اكتمال التحميل وإرجاع عنوان URL الفريد للملف الذي تم تحميله.
بالنسبة للتطبيقات ذات الحجم الكبير للغاية ، Date.now()   لا تضمن الطريقة   100٪ اسم ملف فريد ، ولكن بالنسبة لتطبيق هذا المقياس ، فإن هذه الطريقة كافية إلى حد كبير.
الآن بعد أن أصبحت الخدمة جاهزة ، ستضيف الوظائف اللازمة إليها   BookFormComponent  .
ابدأ بإضافة بعض الأعضاء الإضافيين إلى المكون:

bookForm: FormGroup;
fileIsUploading = false;
fileUrl: string;
fileUploaded = false;

    bookForm: FormGroup;
    fileIsUploading = false;
    fileUrl: string;
    fileUploaded = false;
ثم قم بإنشاء الطريقة التي ستقوم بتشغيل uploadFile() واسترداد عنوان URL الذي تم إرجاعه:

onUploadFile(file: File) {
    this.fileIsUploading = true;
    this.booksService.uploadFile(file).then(
      (url: string) => {
        this.fileUrl = url;
        this.fileIsUploading = false;
        this.fileUploaded = true;
      }
    );
}
ستستخدم   fileIsUploading  لتعطيل زر     القالب submit أثناء تحميل الملف لتجنب أي أخطاء - بمجرد اكتمال التحميل ، يحفظ المكون عنوان URL الذي تم إرجاعه   fileUrl  ويغير حالة المكون ليقول أن التحميل قد اكتمل.
من الضروري إجراء تعديل طفيف   onSaveBook()  لأخذ عنوان URL الخاص بالصورة في الاعتبار إذا كان موجودًا:

onSaveBook() {
    const title = this.bookForm.get('title').value;
    const author = this.bookForm.get('author').value;
    const synopsis = this.bookForm.get('synopsis').value;
    const newBook = new Book(title, author);
    newBook.synopsis = synopsis;
    if(this.fileUrl && this.fileUrl !== '') {
      newBook.photo = this.fileUrl;
    }
    this.booksService.createNewBook(newBook);
    this.router.navigate(['/books']);
}
ستقوم بإنشاء طريقة تربط <input type="file"> (التي ستنشئها لاحقًا) بالطريقة onUploadFile() :

detectFiles(event) {
    this.onUploadFile(event.target.files[0]);
}
يتم إرسال الحدث إلى هذه الطريقة من هذا القسم الجديد من القالب:

<div class="form-group">
    <h4>Ajouter une photo</h4>
    <input type="file" (change)="detectFiles($event)"
           class="form-control" accept="image/*">
    <p class="text-success" *ngIf="fileUploaded">Fichier chargé !</p>
</div>
<button class="btn btn-success" [disabled]="bookForm.invalid || fileIsUploading"
      type="submit">Enregistrer
</button>
بمجرد أن يختار المستخدم ملفًا ، يتم تشغيل الحدث وتحميل الملف. النص "تم تحميل الملف!" يتم عرضه عندما يكون   fileUploaded  غير   true ، ويتم تعطيل الزر عندما يكون النموذج غير صالح أو عندما    تكون fileIsUploading  تساوى   true  .
يبقى فقط لعرض الصورة ، إن وجدت ، في   SingleBookComponent  :

<div class="row">
  <div class="col-xs-12">
    <img style="max-width:400px;" *ngIf="book.photo" [src]="book.photo">
    <h1>{ { book.title }}</h1>
    <h3>{ { book.author }}</h3>
    <p>{ { book.synopsis }}</p>
    <button class="btn btn-default" (click)="onBack()">Retour</button>
  </div>
</div>
يجب أن يؤخذ في الاعتبار أيضًا أنه إذا تم حذف كتاب ، فيجب أيضًا حذف الصورة. الطريقة الجديدة   removeBook()  هي كما يلي:

removeBook(book: Book) {
    if(book.photo) {
      const storageRef = firebase.storage().refFromURL(book.photo);
      storageRef.delete().then(
        () => {
          console.log('Photo removed!');
        },
        (error) => {
          console.log('Could not remove photo! : ' + error);
        }
      );
    }
    const bookIndexToRemove = this.books.findIndex(
      (bookEl) => {
        if(bookEl === book) {
          return true;
        }
      }
    );
    this.books.splice(bookIndexToRemove, 1);
    this.saveBooks();
    this.emitBooks();
}
نظرًا لأن المرجع مطلوب لحذف ملف باستخدام الطريقة   delete() ، فإنك تمرر عنوان URL للملف   refFromUrl()  لاسترداد المرجع.

نشر التطبيق الخاص بك


كفى ! التطبيق جاهز للنشر. إذا كان خادم التطوير لا يزال قيد التشغيل ، فأوقفه وشغّل الأمر التالي:

ng build –prod
يمكنك استخدام CLI لإنشاء حزمة الإنتاج النهائية للتطبيق الخاص بك في المجلد   dist  .
البناء هو وقت يمكن أن يحدث فيه العديد من الأخطاء ، ولن يكون بالضرورة بسبب التعليمات البرمجية الخاصة بك. من جانبي ، كان عليّ تحديث CLI وتغيير الإصدار في devDependencies في package.json . للأسف ، لا يمكننا التنبؤ بالأخطاء التي قد تحدث في ذلك الوقت ، لكنك لن تكون الوحيد الذي يواجه مشكلتك: قم بنسخ الخطأ في محرك البحث المفضل لديك وستجد بالتأكيد إجابة.
  يحتوي المجلد Dist   على جميع الملفات المراد تحميلها على خادم النشر لتطبيقك.

الخلاصة


مبروك! لقد استخدمت مهاراتك الجديدة في Angularلإنشاء تطبيق ديناميكي يتألف من عدة مكونات:
  • عرض البيانات في القالب مع ربط البيانات ؛
  • مبنية ديناميكيًا مع إرشادات ؛
  • التواصل معًا من خلال الخدمات ؛
  • يمكن الوصول إليها عن طريق التوجيه الشخصي ؛
  • استخدام Observables لإدارة تدفقات البيانات ؛
  • استخدام النماذج لمعالجة البيانات المقدمة من قبل المستخدم ؛
  • العمل مع الواجهة الخلفية لـFirebaseللمصادقة وإدارة البيانات والملفات.
لديك الآن المعرفة لإنشاء تطبيقAngularالخاص بك والعثور على المعلومات التي تحتاجها لدمج الميزات التي لا تعرفها بعد. لا تتردد في الذهاب إلى أبعد من ذلك ، للبحث عن أفكار جديدة وأساليب جديدة ، وستقوم بالتأكيد بإنشاء تطبيقات رائعة!