Статьи

Аутентификация Firebase и Angular с Auth0: Часть 2

Эта статья была первоначально опубликована в блоге Auth0.com и публикуется здесь с разрешения.

В этой серии из двух частей мы узнаем, как создать приложение, которое защищает внутренний узел Node и внешний интерфейс Angular с аутентификацией Auth0 . Наш сервер и приложение также будут аутентифицировать базу данных Firebase Cloud Firestore с помощью пользовательских токенов, чтобы пользователи могли безопасно оставлять комментарии в реальном времени после входа в систему с помощью Auth0. Код приложения Angular можно найти в репозитории angit-firebase GitHub, а API-интерфейс Node — в репозитории firebase-auth0-nodeserver .

Первая часть нашего урока, Аутентификация Firebase и Angular с Auth0: Часть 1 , охватывала:

  • введение и настройка для Auth0 и Firebase
  • реализация безопасного Node API, который обрабатывает пользовательские токены Firebase и предоставляет данные для нашего приложения
  • Угловая архитектура приложения с модулями и отложенной загрузкой
  • Угловая аутентификация с Auth0 с сервисом и защитой маршрута
  • общие угловые компоненты и сервис API.

Аутентификация Firebase и Angular с Auth0: Часть 2

Часть 2 нашего урока будет охватывать:

  1. Отображение собак: Async и NgIfElse
  2. Детали собаки с параметрами маршрута
  3. Класс модели комментариев
  4. Firebase Cloud Firestore и правила
  5. Компонент комментариев
  6. Компонент формы комментария
  7. Комментарии в реальном времени
  8. Вывод

Наше законченное приложение будет выглядеть примерно так:

Приложение Angular Firebase с пользовательскими токенами Auth0

Давайте выберем прямо там, где мы остановились в конце Аутентификации Firebase и Angular с Auth0: Часть 1 .

Отображение собак: Async и NgIfElse

Давайте реализуем домашнюю страницу нашего приложения — список собак. Мы создали леса для этого компонента при настройке архитектуры нашего приложения Angular.

Важное примечание: убедитесь, что ваш Node.js API работает. Если вам нужно обновить API, обратитесь к разделу Как аутентифицировать Firebase и Angular с Auth0: Часть 1 — Node API .

Компоненты класса собак

Откройте dogs.component.ts файл класса dogs.component.ts и реализуйте этот код:

 // src/app/dogs/dogs/dogs.component.ts import { Component, OnInit } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { ApiService } from '../../core/api.service'; import { Dog } from './../../core/dog'; import { Observable } from 'rxjs/Observable'; import { tap, catchError } from 'rxjs/operators'; @Component({ selector: 'app-dogs', templateUrl: './dogs.component.html' }) export class DogsComponent implements OnInit { pageTitle = 'Popular Dogs'; dogsList$: Observable<Dog[]>; loading = true; error: boolean; constructor( private title: Title, private api: ApiService ) { this.dogsList$ = api.getDogs$().pipe( tap(val => this._onNext(val)), catchError((err, caught) => this._onError(err, caught)) ); } ngOnInit() { this.title.setTitle(this.pageTitle); } private _onNext(val: Dog[]) { this.loading = false; } private _onError(err, caught): Observable<any> { this.loading = false; this.error = true; return Observable.throw('An error occurred fetching dogs data.'); } } 

После нашего импорта мы настроим некоторые локальные свойства:

  • pageTitle : установить <h1> и <title> нашей страницы
  • dogsList$ : наблюдаемое, возвращаемое нашим HTTP-запросом API для получения данных о dogsList$ собак.
  • loading : показать значок загрузки во время выполнения запроса API
  • error : отображать ошибку, если что-то идет не так, выбирая данные из API.

Мы будем использовать декларативный асинхронный канал для ответа на наблюдаемый dogsList$ возвращаемый нашим запросом API GET . Благодаря асинхронному DogsComponent нам не нужно подписываться или отписываться в нашем классе DogsComponent : процесс подписки будет управляться автоматически! Нам просто нужно настроить нашу наблюдаемую.

Мы сделаем Title и ApiService доступными для нашего класса, передав их конструктору, а затем dogsList$ нашу dogsList$ observable. Мы будем использовать операторы RxJS tap (ранее известный как оператор do ) и catchError для вызова функций-обработчиков. Оператор tap выполняет побочные эффекты, но не влияет на передаваемые данные, поэтому он идеально подходит для установки других свойств. Функция _onNext() установит loading в false (поскольку данные были успешно отправлены). Функция _onError() установит loading и error соответственно и выдаст ошибку. Как упоминалось ранее, нам не нужно подписываться или отписываться от наблюдаемого dogsList$ потому что асинхронный канал (который мы добавим в шаблон) будет обрабатывать это для нас.

При инициализации нашего компонента мы будем использовать ngOnInit() чтобы шпионить за ловушкой жизненного цикла OnInit, чтобы установить документ <title> .

Вот и все для нашего класса компонентов Dogs!

Шаблон компонента «Собаки»

Давайте перейдем к шаблону по адресу dogs.component.html :

 <!-- src/app/dogs/dogs/dogs.component.html --> <h1 class="text-center">{{ pageTitle }}</h1> <ng-template #noDogs> <app-loading *ngIf="loading"></app-loading> <app-error *ngIf="error"></app-error> </ng-template> <div *ngIf="dogsList$ | async as dogsList; else noDogs"> <p class="lead"> These were the top <a href="http://www.akc.org/content/news/articles/the-labrador-retriever-wins-top-breed-for-the-26th-year-in-a-row/">10 most popular dog breeds in the United States in 2016</a>, ranked by the American Kennel Club (AKC). </p> <div class="row mb-3"> <div *ngFor="let dog of dogsList" class="col-xs-12 col-sm-6 col-md-4"> <div class="card my-2"> <img class="card-img-top" [src]="dog.image" [alt]="dog.breed"> <div class="card-body"> <h5 class="card-title">#{{ dog.rank }}: {{ dog.breed }}</h5> <p class="text-right mb-0"> <a class="btn btn-primary" [routerLink]="['/dog', dog.rank]">Learn more</a> </p> </div> </div> </div> </div> </div> <app-comments></app-comments> 

В этом шаблоне есть пара вещей, на которые мы подробнее рассмотрим:

 ... <ng-template #noDogs> <app-loading *ngIf="loading"></app-loading> <app-error *ngIf="error"></app-error> </ng-template> <div *ngIf="dogsList$ | async as dogsList; else noDogs"> ... <div *ngFor="let dog of dogsList" ...> ... 

Этот код делает некоторые очень полезные вещи декларативно. Давайте исследуем.

Сначала у нас есть элемент <ng-template> со ссылочной переменной шаблона ( #noDogs ). Элемент <ng-template> никогда не отображается напрямую. Он предназначен для использования со структурными директивами (такими как NgIf). В этом случае мы создали встроенное представление с помощью <ng-template #noDogs> которое содержит компоненты загрузки и ошибки. Каждый из этих компонентов будет отображаться в зависимости от условия. noDogs представление noDogs само не будет отображаться, пока не будет указано.

Так как (и когда) мы говорим этому представлению визуализировать?

Следующий <div *ngIf="... на самом деле является NgIfElse, использующим префикс звездочки в качестве синтаксического сахара . Мы также используем асинхронный канал с нашим dogsList$ наблюдаемым и устанавливаем переменную, чтобы мы могли ссылаться на испущенные значения потока в нашем template ( as dogsList ). Если что-то пойдет не так с dogsList$ observable, у нас есть else noDogs оператор else noDogs который говорит шаблону визуализировать представление <ng-template #noDogs> . Это было бы верно до успешного извлечения данных из API, или если наблюдаемая ошибка была выдана.

Если dogsList$ | async dogsList$ | async успешно отправил значение, div будет отображаться, и мы можем dogsList итерацию по нашему значению dogsList (которое, как ожидается, будет массивом Dog , как указано в нашем классе компонентов), используя структурную директиву NgForOf ( *ngFor ) для отображения каждого Информация о собаке.

Как вы можете видеть в оставшемся HTML, каждая собака будет отображаться с изображением, рангом, породой и ссылкой на их отдельную страницу с подробностями, которую мы создадим дальше.

Просмотрите компонент Dogs в браузере, перейдя на домашнюю страницу вашего приложения по адресу http: // localhost: 4200 . Приложение Angular должно сделать запрос к API, чтобы получить список собак и отобразить их!

Примечание. Мы также включили компонент <app-comments> . Поскольку мы создали этот компонент, но еще не реализовали его функциональность, он должен отображаться в пользовательском интерфейсе в виде текста с надписью «Комментарии работают!»

Чтобы проверить обработку ошибок, вы можете остановить сервер API ( Ctrl+c в командной строке или терминале сервера). Затем попробуйте перезагрузить страницу. Компонент ошибки должен отображаться, так как API недоступен, и мы должны увидеть соответствующие ошибки в консоли браузера:

Угловое приложение с Node.js API, показывающая ошибку данных

Детали собаки с параметрами маршрута

Далее мы реализуем наш компонент Dog. Этот перенаправленный компонент служит страницей сведений для каждой собаки. Мы уже настроили архитектуру нашего модуля Dog вместе с маршрутизацией и отложенной загрузкой в первой части этого урока. Все, что нам нужно сделать сейчас, это реализовать!

Напоминание: Вы можете вспомнить из части 1, что страница с информацией о собаке защищена системой AuthGuard маршрута AuthGuard . Это означает, что посетитель должен пройти аутентификацию для доступа к странице. Кроме того, для вызова API требуется токен доступа для возврата данных.

Собака Компонент Класс

Откройте dog.component.ts класса dog.component.ts и добавьте:

 // src/app/dog/dog/dog.component.ts import { Component, OnInit, OnDestroy } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Title } from '@angular/platform-browser'; import { ApiService } from '../../core/api.service'; import { DogDetail } from './../../core/dog-detail'; import { Subscription } from 'rxjs/Subscription'; import { Observable } from 'rxjs/Observable'; import { tap, catchError } from 'rxjs/operators'; @Component({ selector: 'app-dog', templateUrl: './dog.component.html', styles: [` .dog-photo { background-repeat: no-repeat; background-position: 50% 50%; background-size: cover; min-height: 250px; width: 100%; } `] }) export class DogComponent implements OnInit, OnDestroy { paramSub: Subscription; dog$: Observable<DogDetail>; loading = true; error: boolean; constructor( private route: ActivatedRoute, private api: ApiService, private title: Title ) { } ngOnInit() { this.paramSub = this.route.params .subscribe( params => { this.dog$ = this.api.getDogByRank$(params.rank).pipe( tap(val => this._onNext(val)), catchError((err, caught) => this._onError(err, caught)) ); } ); } private _onNext(val: DogDetail) { this.loading = false; } private _onError(err, caught): Observable<any> { this.loading = false; this.error = true; return Observable.throw('An error occurred fetching detail data for this dog.'); } getPageTitle(dog: DogDetail): string { const pageTitle = `#${dog.rank}: ${dog.breed}`; this.title.setTitle(pageTitle); return pageTitle; } getImgStyle(url: string) { return `url(${url})`; } ngOnDestroy() { this.paramSub.unsubscribe(); } } 

Этот компонент очень похож на наш компонент листинга Dogs, за исключением нескольких ключевых отличий.

Мы импортируем необходимые зависимости и в частном порядке используем ApiService и Title в нашем классе.

Компонент Dog details использует параметр маршрута для определения, для какой собаки нам нужно получить данные. Параметр route соответствует рангу желаемой собаки в списке десяти самых популярных собак, например:

 # URL for dog #2: http://localhost:4200/dog/2 

Чтобы получить доступ к этому параметру в классе компонентов, нам нужно импортировать интерфейс ActivatedRoute , передать его конструктору и подписаться на наблюдаемые params активированного маршрута.

Затем мы можем передать параметр getDogByRank$() нашему сервисному методу API getDogByRank$() . Мы также должны отписаться от параметров маршрута, наблюдаемых при уничтожении компонента . Наша dog$ observable может использовать обработчики tap и catchError аналогичные нашему компоненту листинга Dogs.

Нам также понадобится несколько методов, чтобы помочь нашему шаблону.

Метод getPageTitle() использует данные API для генерации заголовка страницы, который включает ранг и породу собаки.

Метод getImgStyle() использует данные API для возврата значения CSS фонового изображения.

Шаблон компонента «Собака»

Теперь давайте использовать эти методы в нашем шаблоне dog.component.html :

 <!-- src/app/dog/dog/dog.component.html --> <ng-template #noDog> <app-loading *ngIf="loading"></app-loading> <app-error *ngIf="error"></app-error> </ng-template> <div *ngIf="dog$ | async as dog; else noDog"> <h1 class="text-center">{{ getPageTitle(dog) }}</h1> <div class="row align-items-center pt-2"> <div class="col-12 col-sm-6"> <div class="dog-photo rounded mb-2 mb-sm-0" [style.backgroundImage]="getImgStyle(dog.image)"></div> </div> <ul class="list-unstyled col-12 col-sm-6"> <li><strong>Group:</strong> {{ dog.group }}</li> <li><strong>Personality:</strong> {{ dog.personality }}</li> <li><strong>Energy Level:</strong> {{ dog.energy }}</li> </ul> </div> <div class="row"> <div class="col"> <p class="lead mt-3" [innerHTML]="dog.description"></p> <p class="clearfix"> <a routerLink="/" class="btn btn-link float-left">&larr; Back</a> <a class="btn btn-primary float-right" [href]="dog.link" target="_blank">{{ dog.breed }} AKC Info</a> </p> </div> </div> </div> 

В целом, этот шаблон выглядит и функционирует аналогично нашему шаблону компонента списка собак, за исключением того, что мы не выполняем итерацию по массиву. Вместо этого мы показываем информацию только для одной собаки, и заголовок страницы генерируется динамически, а не статично. Мы будем использовать данные об излучаемой dog наблюдаемой (из dog$ | async as dog ), чтобы отобразить детали с помощью CSS- классов Bootstrap .

Компонент должен выглядеть следующим образом в браузере:

Угловое приложение с асинхронным каналом и аутентификацией - деталь собаки

Чтобы попасть на страницу с подробной информацией о любой собаке, AuthGuard войти в систему неаутентифицированному пользователю. После проверки подлинности они будут перенаправлены на страницу запрашиваемых сведений. Попробуйте!

Класс модели комментариев

Теперь, когда наш список собак и подробные страницы готовы, пришло время поработать над добавлением комментариев в реальном времени!

Первое, что мы сделаем, это установим форму наших комментариев, а также способ инициализации новых экземпляров комментариев. Давайте реализуем класс comment.ts в нашем приложении Angular:

 // src/app/comments/comment.ts export class Comment { constructor( public user: string, public uid: string, public picture: string, public text: string, public timestamp: number ) {} // Workaround because Firestore won't accept class instances // as data when adding documents; must unwrap instance to save. // See: https://github.com/firebase/firebase-js-sdk/issues/311 public get getObj(): object { const result = {}; Object.keys(this).map(key => result[key] = this[key]); return result; } } 

В отличие от наших моделей Dog и DogDetail , наша модель Comment является классом , а не интерфейсом . В конечном итоге мы будем инициализировать экземпляры Comment в нашем компоненте формы комментариев, и для этого необходим класс. Кроме того, Firestore принимает только обычные объекты JS при добавлении документов в коллекцию, поэтому нам нужно добавить метод к нашему классу, который разворачивает экземпляр в объект. Интерфейс, с другой стороны, предоставляет только описание объекта. Этого было достаточно для Dog и DogDetail , но для Comment было бы недостаточно.

При визуализации мы хотим, чтобы комментарии выглядели примерно так:

Угловое приложение Firebase с комментариями

Как видите, у каждого комментария есть имя пользователя, изображение, текст комментария, а также дата и время. Для комментариев также необходим уникальный идентификатор, указанный в данных как uid . Этот уникальный идентификатор гарантирует, что пользователи имеют надлежащий доступ для удаления своих комментариев, но не комментариев, оставленных другими.

Теперь, когда у нас есть представление о том, как должен выглядеть комментарий, давайте настроим наши правила Firebase Firestore.

Firebase Cloud Firestore и правила

Мы будем использовать базу данных Firebase Cloud Firestore для хранения комментариев нашего приложения. Cloud Firestore — это NoSQL, гибкая, масштабируемая, размещенная в облаке база данных, которая обеспечивает возможности в реальном времени. На момент написания Firestore находится в бета-версии, но это рекомендуемая база данных для всех новых мобильных и веб-приложений. Вы можете узнать больше о выборе между базой данных реального времени (RTDB) и облачным хранилищем пожаров здесь .

Напоминание: если вам нужно быстрое переподготовка продукта Firebase, перечитайте « Как аутентифицировать Firebase и Angular с Auth0 — Часть 1: Firebase и Auth0» .

Firestore организует данные в виде документов в коллекциях . Эта модель данных должна быть знакома, если у вас есть опыт работы с ориентированными на документы базами данных NoSQL, такими как MongoDB . Давайте теперь выберем Cloud Firestore в качестве нашей базы данных.

  1. Войдите в проект Firebase, который вы создали в первой части этого руководства .
  2. Нажмите на базу данных в боковом меню.
  3. В раскрывающемся списке рядом с заголовком страницы базы данных выберите Cloud Firestore .

Добавить коллекцию и первый документ

Вкладка « Данные » будет отображаться по умолчанию, и в базе данных в настоящее время ничего нет. Давайте добавим нашу коллекцию и документ, чтобы мы могли запрашивать нашу базу данных на Angular и что-то возвращать.

Нажмите на + Добавить коллекцию . Назовите comments своей коллекции, затем нажмите кнопку « Далее» . Вам будет предложено добавить свой первый документ.

Консоль Firebase - добавить документ

В поле « Идентификатор документа» нажмите « Автоидентификация» . Это автоматически заполнит идентификатор для вас. Затем добавьте поля, которые мы установили ранее в модели comment.ts с соответствующими типами и некоторыми данными-заполнителями. Нам нужен только этот исходный документ, пока мы не узнаем, что наш листинг правильно отображается в нашем приложении Angular, затем мы можем удалить его с помощью консоли Firebase и правильно ввести комментарии, используя форму в интерфейсе пользователя.

Однако, поскольку у нас еще нет построенной формы, начальные данные будут полезны. После того, как вы ввели правильные поля и типы, вы можете заполнить значения так, как вам нравится. Вот предложение:

 user <string>: Test User uid <string>: abc-123 picture <string>: https://cdn.auth0.com/avatars/tu.png text <string>: This is a test comment from Firebase console. timestamp <number>: 1514584235257 

Примечание. Комментарий с uid значением uid не будет действителен для любого подлинно прошедшего проверку пользователя после того, как мы настроим правила безопасности Firebase. Начальный документ нужно будет удалить с помощью консоли Firebase, если мы захотим удалить его позже. У нас не будет доступа для его удаления с помощью методов SDK в приложении Angular, как вы увидите в правилах ниже.

После того, как вы ввели комментарий вашего фальшивого пользователя, нажмите кнопку Сохранить . Новая коллекция и документ должны быть заполнены в базе данных. Это предоставляет данные, которые мы можем запросить в нашем приложении Angular.

Правила Firebase

Далее давайте настроим безопасность нашей базы данных Firestore. Перейдите на вкладку « Правила ».

Правила безопасности Firebase обеспечивают внутреннюю безопасность и проверку . В Node API нашего приложения мы убедились, что пользователи авторизованы для доступа к конечным точкам с использованием промежуточного программного обеспечения аутентификации Auth0 и JWT . Мы уже настроили проверку подлинности Firebase в нашем API и приложении Angular, и мы будем использовать функцию правил для авторизации разрешений на стороне базы данных.

Правило — это выражение, которое оценивается, чтобы определить, разрешено ли запросу выполнить желаемое действие. Справочник по правилам безопасности Cloud Firestore

Добавьте следующий код в редактор правил базы данных Firebase. Мы рассмотрим это более подробно ниже.

 // Firebase Database Rules for Cloud Firestore service cloud.firestore { match /databases/{database}/documents { match /comments/{document=**} { allow read: if true; allow create: if request.auth != null && request.auth.uid == request.resource.data.uid && request.resource.data.text is string && request.resource.data.text.size() <= 200; allow delete: if request.auth != null && request.auth.uid == resource.data.uid; } } } 

У Firestore есть методы запроса правил : read и write . Read включает операции get и list . Запись включает операции create , update и delete . Мы реализуем правила read , create и delete .

Примечание. Мы не будем добавлять функцию редактирования комментариев в наше приложение, поэтому update не включено. Тем не менее, не стесняйтесь добавлять правило update если вы хотите добавить эту функцию самостоятельно!

Правила выполняются, когда пользовательский запрос match пути к документу. Пути могут быть полностью названы, или они могут использовать подстановочные знаки. Наши правила применяются ко всем документам в коллекции comments мы создали.

Мы хотим, чтобы каждый мог читать комментарии, как анонимные, так и аутентифицированные пользователи. Следовательно, условие для allow read просто, if true .

Мы хотим, чтобы только прошедшие проверку пользователи могли создавать новые комментарии. Мы проверим, что пользователь вошел в систему, и удостоверимся, что сохраняемые данные имеют свойство uid , соответствующее uid аутентификации пользователя ( request.auth.uid в правилах Firebase). Кроме того, мы можем сделать небольшую проверку поля здесь. Мы проверим, что данные запроса имеют text свойство, которое является строкой и имеет длину не более 200 символов (мы также вскоре добавим эту проверку в наше приложение Angular).

Наконец, мы хотим, чтобы пользователи могли удалять свои комментарии. Мы можем allow delete если UID аутентифицированного пользователя соответствует существующему свойству uid комментария, используя resource.data.uid .

Примечание. Подробнее о ключевых словах запроса и ресурса вы можете узнать из документации Firebase.

Компонент комментариев

Теперь, когда наша база данных готова, пришло время вернуться к нашему приложению Angular и реализовать комментирование в реальном времени!

Первое, что мы сделаем, это отобразим комментарии. Мы хотим, чтобы комментарии обновлялись асинхронно в реальном времени, поэтому давайте рассмотрим, как это сделать с нашей базой данных Cloud Firestore и angularfire2 SDK .

Класс компонента комментариев

Мы уже создали архитектуру для нашего модуля Комментарии, поэтому давайте начнем с создания нашего comments.component.ts :

 // src/app/comments/comments/comments.component.ts import { Component } from '@angular/core'; import { AngularFirestore, AngularFirestoreCollection, AngularFirestoreDocument } from 'angularfire2/firestore'; import { Observable } from 'rxjs/Observable'; import { map, catchError } from 'rxjs/operators'; import { Comment } from './../comment'; import { AuthService } from '../../auth/auth.service'; @Component({ selector: 'app-comments', templateUrl: './comments.component.html', styleUrls: ['./comments.component.css'] }) export class CommentsComponent { private _commentsCollection: AngularFirestoreCollection<Comment>; comments$: Observable<Comment[]>; loading = true; error: boolean; constructor( private afs: AngularFirestore, public auth: AuthService ) { // Get latest 15 comments from Firestore, ordered by timestamp this._commentsCollection = afs.collection<Comment>( 'comments', ref => ref.orderBy('timestamp').limit(15) ); // Set up observable of comments this.comments$ = this._commentsCollection.snapshotChanges() .pipe( map(res => this._onNext(res)), catchError((err, caught) => this._onError(err, caught)) ); } private _onNext(res) { this.loading = false; this.error = false; // Add Firestore ID to comments // The ID is necessary to delete specific comments return res.map(action => { const data = action.payload.doc.data() as Comment; const id = action.payload.doc.id; return { id, ...data }; }); } private _onError(err, caught): Observable<any> { this.loading = false; this.error = true; return Observable.throw('An error occurred while retrieving comments.'); } onPostComment(comment: Comment) { // Unwrap the Comment instance to an object for Firestore // See https://github.com/firebase/firebase-js-sdk/issues/311 const commentObj = <Comment>comment.getObj; this._commentsCollection.add(commentObj); } canDeleteComment(uid: string): boolean { if (!this.auth.loggedInFirebase || !this.auth.userProfile) { return false; } return uid === this.auth.userProfile.sub; } deleteComment(id: string) { // Delete comment with confirmation prompt first if (window.confirm('Are you sure you want to delete your comment?')) { const thisDoc: AngularFirestoreDocument<Comment> = this.afs.doc<Comment>(`comments/${id}`); thisDoc.delete(); } } } 

Сначала мы импортируем необходимые зависимости angularfire2 для использования Firestore, коллекций и документов. Нам также нужны Observable , map и catchError из RxJS, нашей модели Comment и AuthService .

Мы объявим участников следующим. Частная _commentsCollection — это коллекция Firestore, содержащая элементы в форме Comment . comments$ observable — это поток со значениями, которые принимают форму массивов Comment s. Тогда у нас есть обычные свойства loading и error .

После передачи AngularFirestore и AuthService в функцию конструктора, нам нужно извлечь данные нашей коллекции из Cloud Firestore. Для этого мы будем использовать метод collection() angularfire2 , указав тип Comment в качестве типа, передав имя нашей коллекции ( comments ), упорядочив результаты по timestamp и ограничив последними 15 комментариями.

Далее мы создадим наши comments$ observable с помощью _commentsCollection . Мы будем использовать RxJS операторы map() и catchError() для обработки данных и ошибок.

В нашем частном обработчике _onNext() мы установим _onNext() и error в false . Мы также добавим идентификатор документа Firestore для каждого элемента в массивах, генерируемых потоком comments$ . Нам нужны эти идентификаторы, чтобы пользователи могли удалять отдельные комментарии. Чтобы добавить идентификатор к выданным значениям, мы будем использовать метод snapshotChanges() для доступа к метаданным . Затем мы можем map() id документа map() в возвращенные данные, используя оператор распространения .

Примечание. Вы можете заметить, что мы не установили error в false в методе успеха для наших собак или наблюдаемых собак, но мы делаем это здесь. Поток комментариев выдает значение каждый раз, когда любой пользователь добавляет комментарий в режиме реального времени. Поэтому в ответ может потребоваться асинхронно сбросить статус ошибки.

_onError() обработчик _onError() должен выглядеть очень знакомым из наших других компонентов. Он устанавливает свойства loading и error и выдает ошибку.

Метод onPostComment() будет запущен, когда пользователь отправит комментарий, используя компонент формы комментария (который мы вскоре создадим). onPostComment() нагрузка onPostComment() будет содержать экземпляр Comment содержащий данные комментария пользователя, которые затем необходимо развернуть в обычный объект, чтобы сохранить в Firestore. Мы сохраним развернутый объект комментария, используя метод Angular Firestore add() .

Метод canDeleteComment() проверяет, является ли текущий пользователь владельцем данного комментария. Если они создали комментарий, они также могут удалить его. Этот метод проверяет, что свойство userProfile.sub зарегистрированного пользователя соответствует uid комментария.

Метод deleteComment() будет запущен, когда пользователь deleteComment() значок, чтобы удалить комментарий. Этот метод открывает диалоговое окно подтверждения, которое подтверждает действие и, если оно подтверждено, использует аргумент id для удаления правильного документа комментария из коллекции Firestore. (Вот почему нам нужно было добавить id документов в наши данные, когда мы отобразили значения, испускаемые нашими comments$ observable.)

Примечание. Напомним, что наши правила Firestore также запрещают пользователям удалять комментарии, которые они не создавали. Мы всегда должны обеспечивать соблюдение прав доступа как на передней, так и на внутренней стороне для обеспечения надлежащей безопасности.

Шаблон компонента комментариев

Теперь давайте добавим функциональность нашего класса для работы в пользовательском интерфейсе. Откройте файл comments.component.html и добавьте:

 <!-- src/app/comments/comments/comments.component.html --> <section class="comments py-3"> <h3>Comments</h3> <ng-template #noComments> <p class="lead" *ngIf="loading"> <app-loading [inline]="true"></app-loading>Loading comments... </p> <app-error *ngIf="error"></app-error> </ng-template> <div *ngIf="comments$ | async; let commentsList; else noComments"> <ul class="list-unstyled"> <li *ngFor="let comment of commentsList" class="pt-2"> <div class="row mb-1"> <div class="col"> <img [src]="comment.picture" class="avatar rounded"> <strong>{{ comment.user }}</strong> <small class="text-info">{{ comment.timestamp | date:'short' }}</small> <strong> <a *ngIf="canDeleteComment(comment.uid)" class="text-danger" title="Delete" (click)="deleteComment(comment.id)">&times;</a> </strong> </div> </div> <div class="row"> <div class="col"> <p class="comment-text rounded p-2 my-2" [innerHTML]="comment.text"></p> </div> </div> </li> </ul> <div *ngIf="auth.loggedInFirebase; else logInToComment"> <app-comment-form (postComment)="onPostComment($event)"></app-comment-form> </div> <ng-template #logInToComment> <p class="lead" *ngIf="!auth.loggedIn"> Please <a class="text-primary" (click)="auth.login()">log in</a> to leave a comment. </p> </ng-template> </div> </section> 

В основном мы будем использовать классы Bootstrap для стилизации наших комментариев с небольшим количеством пользовательских CSS, которые мы добавим дальше. Наш шаблон комментариев, как и наши шаблоны компонентов dog и dog, имеет <ng-template> и использует асинхронный канал с NgIfElse для отображения соответствующего пользовательского интерфейса.

Список комментариев должен содержать изображение комментария (аватар пользователя и его автора), имя пользователя и отметку timestamp отформатированную с помощью DatePipe . Мы передадим uid комментария в метод canDeleteComment() чтобы определить, должна ли быть показана ссылка для удаления. Затем мы отобразим text комментария, используя привязку свойства к innerHTML .

Наконец, мы создадим элементы, чтобы показать форму комментария или сообщение, инструктирующее пользователей войти в систему, если они хотят оставить комментарий.

Примечание. Наша <app-comment-form> будет использовать привязку события для postComment события с именем postComment когда пользователь отправляет комментарий. Класс CommentsComponent прослушивает это событие и обрабатывает его с помощью onPostComment() который мы создали, используя полезную нагрузку $event для сохранения отправленного комментария в базу данных Firestore. Мы подключим событие (postComment) при создании формы в следующем разделе.

Компонент комментариев CSS

Наконец, откройте файл comments.component.css и добавим несколько стилей в наш список комментариев:

 /* src/app/comments/comments/comments.component.css */ .avatar { display: inline-block; height: 30px; } .comment-text { background: #eee; position: relative; } .comment-text::before { border-bottom: 10px solid #eee; border-left: 6px solid transparent; border-right: 6px solid transparent; content: ''; display: block; height: 1px; position: absolute; top: -10px; left: 9px; width: 1px; } 

Компонент формы комментария

Теперь, когда у нас есть список комментариев, которые обновляются в режиме реального времени, мы должны иметь возможность добавлять новые комментарии в наш интерфейс.

Класс компонента формы комментария

Откройте файл comment-form.component.ts и начнем:

 // src/app/comments/comment-form/comment-form.component.ts import { Component, OnInit, Output, EventEmitter } from '@angular/core'; import { Comment } from './../../comment'; import { AuthService } from '../../../auth/auth.service'; @Component({ selector: 'app-comment-form', templateUrl: './comment-form.component.html' }) export class CommentFormComponent implements OnInit { @Output() postComment = new EventEmitter<Comment>(); commentForm: Comment; constructor(private auth: AuthService) { } ngOnInit() { this._newComment(); } private _newComment() { this.commentForm = new Comment( this.auth.userProfile.name, this.auth.userProfile.sub, this.auth.userProfile.picture, '', null); } onSubmit() { this.commentForm.timestamp = new Date().getTime(); this.postComment.emit(this.commentForm); this._newComment(); } } 

Как упоминалось ранее, нам нужно будет отправить событие из этого компонента в родительский CommentsComponent , который отправит новый комментарий в Firestore. CommentFormComponent отвечает за создание экземпляра Comment с соответствующей информацией, полученной от аутентифицированного пользователя, от ввода его формы и отправки этих данных родителю. Чтобы postComment событие postComment , мы импортируем Output и EventEmitter . Нам также понадобится наш класс Comment и AuthService для получения пользовательских данных.

В состав нашего компонента формы комментариев входят декоратор вывода ( postComment ), который является EventEmitter с типом Comment , и commentForm , который будет экземпляром Comment для хранения данных формы.

В нашем ngOnInit() мы создадим новый экземпляр Comment с закрытым _newComment() . Этот метод устанавливает локальное свойство commentForm для нового экземпляра Comment с name , sub и picture аутентифицированного пользователя. Текст комментария является пустой строкой, а timestamp имеет значение null (оно будет добавлено при отправке формы).

Метод onSubmit() будет выполнен, когда форма комментария будет отправлена ​​в шаблон. Этот метод добавляет timestamp и генерирует событие commentForm данными commentForm качестве полезной нагрузки. Он также вызывает метод _newComment() для сброса формы комментария.

Шаблон компонента формы комментария

Откройте файл comment-form.component.html и добавьте этот код:

 <!-- src/app/comments/comment-form/comment-form.component.html --> <form (ngSubmit)="onSubmit()" #tplForm="ngForm"> <div class="row form-inline m-1"> <input type="text" class="form-control col-sm-10 mb-2 mb-sm-0" name="text" [(ngModel)]="commentForm.text" maxlength="200" required> <button class="btn btn-primary col ml-sm-2" [disabled]="!tplForm.valid">Send</button> </div> </form> 

Шаблон формы комментария довольно прост. Единственное поле формы — это текстовый ввод, поскольку все остальные данные комментариев (например, имя, изображение, UID и т. Д.) Динамически добавляются в класс. Мы будем использовать простую форму на основе шаблонов для реализации нашей формы комментариев.

Элемент <form> прослушивает событие (ngOnSubmit) , которое мы обработаем с помощью нашего onSubmit() . Мы также добавим ссылочную переменную шаблона с именем #tplForm и установим ее в ngForm . Таким образом, мы можем получить доступ к свойствам формы в самом шаблоне.

Элемент <input> должен иметь [(ngModel)] который связывается с commentForm.text . Это свойство, которое мы хотим обновить, когда пользователь вводит данные в поле формы. Напомним, что мы настроили наши правила Firestore для приема текста комментария не более 200 символов, поэтому мы добавим эту maxlength в наш интерфейс вместе с required атрибутом, чтобы пользователи не могли отправлять пустые комментарии.

Наконец, <button> для отправки формы должна быть [disabled] если форма недействительна. Мы можем ссылаться на valid свойство с tplForm ссылочной переменной tplForm мы добавили в элемент <form> .

Комментарии в реальном времени

В браузере убедитесь, что комментарии отображаются так, как ожидалось. Пока единственным комментарием должен быть начальный комментарий, который мы добавили непосредственно в Firebase. При получении и обработке наш список комментариев должен выглядеть следующим образом:

Firebase Firestore комментирует с помощью Angular

Форма комментария должна отображаться, если пользователь аутентифицирован. Войдите и попробуйте добавить комментарий.

Удалить комментарий к семени

Пользователи могут удалять свои собственные комментарии. Красный x должен появиться рядом с датой и временем комментария, если пользователь является владельцем комментария. Нажатие на этот значок удаления запрашивает подтверждение, а затем удаляет комментарий в режиме реального времени.

Помните, что исходный документ, который мы добавили в Firebase, нельзя удалить в приложении Angular, поскольку его свойство uid не соответствует данным реального пользователя. Давайте удалим это вручную сейчас.

Откройте консоль Firebase и просмотрите коллекцию comments Firestore. Найдите документ, содержащий начальный комментарий. Используя выпадающее меню в правом верхнем углу, выберите « Удалить документ», чтобы удалить его:

Firebase удалить комментарий

Теперь любые комментарии, добавленные в нашу базу данных, могут быть удалены их автором в конце.

Добавить комментарии в приложение Angular

Когда комментарии добавляются, они должны отображаться, и это замечательно, но на самом деле это не демонстрирует истинную природу нашей базы данных Firestore в реальном времени . Мы могли бы добавлять комментарии в пользовательском интерфейсе без обновления, используя традиционный сервер и базу данных, просто обновив представление.

Угловая форма с Firebase Firestore

Чтобы действительно увидеть нашу базу данных в реальном времени, откройте приложение во втором браузере и выполните аутентификацию, используя другой логин. С обоими браузерами добавьте комментарий в одном браузере. Он появится во втором браузере одновременно.

комментирование в реальном времени с Firestore в Angular

Это то, что могут делать базы данных Firebase в реальном времени!

Вывод

Поздравляем! Теперь у вас есть приложение Angular, которое аутентифицирует Firebase с Auth0 и построено на масштабируемой архитектуре.

Первая часть нашего урока, Как аутентифицировать Firebase и Angular с Auth0: Часть 1 , охватывала:

  • введение и настройка для Auth0 и Firebase
  • реализация безопасного Node API, который обрабатывает пользовательские токены Firebase и предоставляет данные для нашего приложения
  • Угловая архитектура приложения с модулями и отложенной загрузкой
  • Угловая аутентификация с Auth0 с сервисом и защитой маршрута
  • общие угловые компоненты и сервис API.

Вторая часть нашего урока посвящена:

  • отображение данных с помощью асинхронного канала и NgIfElse
  • используя параметры маршрута
  • моделирование данных с помощью класса
  • База данных Firebase Cloud Firestore и правила безопасности
  • реализация базы данных Firestore в Angular с помощью angularfire2
  • простая шаблонно-управляемая форма с компонентным взаимодействием.

Угловые ресурсы тестирования

Если вы хотите узнать больше о тестировании в Angular, которое мы не рассмотрели в этом руководстве, ознакомьтесь с некоторыми из следующих ресурсов:

Дополнительные ресурсы

Вы можете найти больше ресурсов по Firebase, Auth0 и Angular здесь:

Что дальше?

Надеюсь, вы многое узнали о создании масштабируемых приложений с Angular и аутентификации Firebase с помощью пользовательских токенов. Если вы ищете идеи для расширения того, что мы создали, вот несколько советов:

  • внедрить неподходящий языковой фильтр для комментариев
  • реализовать роли авторизации для создания пользователя-администратора с правами на удаление комментариев других людей
  • добавить функциональность для поддержки редактирования комментариев
  • добавлять комментарии к отдельным страницам с подробностями о собаках, используя дополнительные коллекции Firestore
  • добавить тестирование
  • и многое другое!