Diese Anwendung wird mit Angular (Version 12.2.8) gestellt. Dies ist eine serverseitige Rendering-App, die node.js
und express
verwendet und nach Filmtiteln sucht. Diese Single -Page -App wird kostenlos auf Heroku (Cloud -Anwendungsplattform) gehostet. Sie müssen ein kostenloses Konto bei theoviedb.org erstellen, um an diesem Tutorial teilzunehmen. Die Anweisungen zur Beschaffung eines TMDB -API -Schlüssels finden Sie unten.
Automatisch lokal durch Angular CLI installiert
ng new movie-search --skip-git
Would you like to add Angular routing? Yes
Which stylesheet format would you like to use? SCSS
Der Vorteil, diese App-Server-Seite zu erstellen, besteht darin, dass die Anwendung auf dem Bildschirm schneller angezeigt wird als eine reguläre App. Dies gibt den Benutzern die Möglichkeit, das Anwendungslayout anzuzeigen, bevor es vollständig interaktiv wird. Es kann auch bei SEO helfen.
ng add @nguniversal/express-engine
server.ts
-Datei erstellt, die die Daten für diese App bedient und verarbeitet.package.json
ändern line 12
in "serve:ssr": "node dist/server/main.js",
angular.json
ändern line 20
in "outputPath": "dist/browser",
und line 129
zu "outputPath": "dist/server",
server.ts
ändern line 14
in const distFolder = join(process.cwd(), 'dist/browser');
npm run build:ssr
npm run serve:ssr
ng gc modules/home --module=app.module.ts
homeComponent
hinweist. //app-routing.module.ts
import { HomeComponent } from './modules/home/home.component' ;
const routes : Routes = [
{
path : '' ,
component : HomeComponent
} ,
{
path : '**' ,
component : HomeComponent
}
] ;
app.component.html
Platzhalter HTML -Inhalte im Folgenden. <!-- app.component.html -->
< div class =" container " >
< router-outlet > </ router-outlet >
</ div >
Für diese App müssen wir die HttpClinetModule
, ReactiveFormsModule
und FormsModule
zur Datei app.module.ts
hinzufügen und den imports
hinzufügen. Dadurch wird diese Module über die gesamte App verfügbar. Das HTTP -Modul ermöglicht es uns, Anrufe an den Server zu tätigen. Die ReactiveFormsModule
hilft uns, FormControl
für die HTML -Eingabe zu verwenden, und wenn sich der Wert ändert (Text in der Sucheingabe) wird eine API -Anforderung an den Server gesendet.
//app.module.ts
import { HttpClientModule } from "@angular/common/http" ;
import { ReactiveFormsModule , FormsModule } from "@angular/forms" ;
FormControl
in home.component.html
.async Observable
und eine function
auf, um auf das Problem zu feuern. <!-- home.component.html -->
< div class =" row " >
< input type =" search " class =" form-control " placeholder =" search " [formControl] =" searchField " >
< span class =" search-title " > {{ (results | async)?.length ? 'Results' : 'Search' }} </ span >
</ div >
//home.component.ts
import { Component , OnInit } from '@angular/core' ;
import { FormControl } from "@angular/forms" ;
import { Observable } from 'rxjs' ;
import {
debounceTime ,
distinctUntilChanged ,
tap ,
switchMap
} from 'rxjs/operators' ;
import { DataService } from '../../services/data.service' ;
@ Component ( {
selector : 'app-home' ,
templateUrl : './home.component.html' ,
styleUrls : [ './home.component.scss' ]
} )
export class HomeComponent implements OnInit {
public loading : boolean = false ;
public results : Observable < any > ;
public searchField : FormControl ;
constructor ( private dataService : DataService ) { }
ngOnInit ( ) : void {
this . searchField = new FormControl ( ) ;
this . results = this . searchField . valueChanges . pipe (
debounceTime ( 400 ) ,
distinctUntilChanged ( ) ,
tap ( _ => {
this . loading = true ;
} ) ,
switchMap ( term => this . dataService . search ( term ) ) ,
tap ( _ => ( this . loading = false ) )
) ;
}
}
Angular cli
-Befehl in das Terminal: ng gs services/data
server.ts
zu senden. //data.service.ts
import { Injectable } from '@angular/core' ;
import { HttpClient , HttpHeaders } from '@angular/common/http' ;
import { Observable } from 'rxjs' ;
const headers = new HttpHeaders ( ) . set ( 'Content-Type' , 'application/X-www-form-urlencoded' ) ;
@ Injectable ( {
providedIn : 'root'
} )
export class DataService {
public result : any ;
constructor ( private http : HttpClient ) { }
search ( item : string ) : Observable < any > {
let searchterm = `query= ${ item } ` ;
try {
this . result = this . http . post ( '/search' , searchterm , { headers } ) ;
return this . result ;
} catch ( e ) {
console . log ( e , 'error' )
}
}
}
In server.ts
erstellen Sie eine Funktion, um die Anforderung vom Client zu verarbeiten. Senden Sie dann die Anfrage an den TMDB -Endpunkt. https://api.themoviedb.org/3/search/movie?api_key=${apiKey}&language=en-US&page=1&include_adult=false&query=${term}
//server.ts line 35-40
server . post ( '/search' , async ( req , res ) => {
let searchquery = req . body . query ;
let encsearchquery = encodeURIComponent ( searchquery ) ;
const data = await api . data . search ( encsearchquery , apiKey ) ;
res . status ( 200 ) . json ( data ) ;
} )
api
-Ordner und api.ts
-Datei.mkdir api
dann berühren Sie cd api
dann touch api.ts
um ein API -Verzeichnis einzurichten.api
-Datei in die Datei server.ts
-Datei. import { api } from './api/api'
. Wenn Sie der TMDB api
in Zukunft verschiedene Anfragen hinzufügen möchten, können Sie sie zu api.ts
hinzufügen, um die server.ts
weniger überfüllt zu halten.
//api.ts
let request = require ( 'request' ) ;
let methods : any = { } ;
let searchInfo = [ ] ;
methods . search = async ( term : string , apiKey : string ) => {
let searchQuery = `https://api.themoviedb.org/3/search/movie?api_key= ${ apiKey } &language=en-US&page=1&include_adult=false&query= ${ term } ` ;
let searchPromise = new Promise ( ( resolve , reject ) => {
request ( searchQuery , { } , function ( err , res , body ) {
let data = JSON . parse ( body ) ;
searchInfo = data [ 'results' ] ;
resolve ( ) ;
} ) ;
} ) ;
let result = await searchPromise ;
return searchInfo ;
}
export const api = { data : methods } ;
Ich verwende die request
, um die API -Anfrage zu behandeln und die Antwort zu analysieren. In TypeScript kann ich Versprechen verwenden, um zu warten, bis die Antwort bereit ist, um einen Fehler zu vermeiden.
settings
.API
-Link und senden Sie Ihre App -Details, um einen API -Schlüssel zu erhalten.Application Name: Movie Search
Application URL: localhost:4000
Application Summary: an app that will search for movies that are related to the search term entered into the app input and display them in the ui.
server.ts
-Datei in Ihrer App hinzu.WARNING: Do not commit your api key to github. If you do it could be found and used by another party.
//server.ts
import 'zone.js/dist/zone-node' ;
import { ngExpressEngine } from '@nguniversal/express-engine' ;
import * as express from 'express' ;
import { join } from 'path' ;
import { enableProdMode } from '@angular/core' ;
import { AppServerModule } from './src/main.server' ;
import { APP_BASE_HREF } from '@angular/common' ;
import { existsSync } from 'fs' ;
import { api } from './api/api' ;
const bodyParser = require ( 'body-parser' ) ;
enableProdMode ( ) ;
// The Express app is exported so that it can be used by serverless Functions.
export function app ( ) : express . Express {
const server = express ( ) ;
const distFolder = join ( process . cwd ( ) , 'dist/browser' ) ;
const indexHtml = existsSync ( join ( distFolder , 'index.original.html' ) ) ? 'index.original.html' : 'index' ;
const apiKey = 'TMDB api key' ;
server . use ( bodyParser . urlencoded ( { extended : true } ) ) ;
...
Wenn wir die Datenantwort vom TMDB-Endpunkt zurückholen, wird sie an den Client zurückgesandt (Front-End). Die home.component.html
muss eingerichtet werden, um ein async Observable
anzuzeigen.
<!-- home.component.html -->
< div class =" center-header " >
< h1 > Movie Search </ h1 >
< i > * This product uses the TMDb API but is not endorsed or certified by TMDb. </ i >
</ div >
< div class =" row " >
< input type =" search " class =" form-control " placeholder =" search " [formControl] =" searchField " >
< span class =" search-title " > {{ (results | async)?.length ? 'Results' : 'Search' }} </ span >
</ div >
< div class =" row wrapper " >
< div class =" no-res " *ngIf =" (results | async)?.length === 0 " > Nothing matches this search. </ div >
< div [ngClass] =" {'dn': item?.poster_path == null} " class =" col " *ngFor =" let item of results | async " >
< span class =" item " >
< span class =" bg " [ngStyle] =" {'background': 'linear-gradient(-225deg, rgba(0,0,0,0.5) 50%, rgba(0,0,0,0.5) 80%), url(https://image.tmdb.org/t/p/w440_and_h660_face'+ item?.poster_path +')' } " >
</ span >
</ span >
</ div >
</ div >
Es gibt ein paar Dinge, die in dieser Benutzeroberfläche ausgepackt werden müssen. Ich verwende eine ternäre Kondung in einer Winkelinterpolationshalterung, um den Text "Search" oder "Ergebnisse" anzuzeigen, der sich abhängt, wenn Daten angezeigt werden.
{{ (results | async)?.length ? 'Results' : 'Search' }}
Ich verwende die [ngClass]
-Richtlinie, die vom Winkelgerüst abhängt. Ich füge die Klasse dn
hinzu, wenn das Data poster_path
null
ist und dann in styles.scss
.dn {display: none;}
um leere Filmelemente zu vermeiden. Ich verwende auch die Direktive [ngStyle]
um das Hintergrundbild der Poster -Bild jedes Filmelements dynamisch hinzuzufügen.
Ich habe einige grundlegende CSS -Stile hinzugefügt, um Filmergebnisse in einem Spaltenlayout von Flex Row zu zeigen. Dies wird auch kleinere mobile Bildschirme behandeln. Mit einer scss
-Datei können Sie verschachtelte CSS wie unten gezeigt schreiben. Vollständige SCSS -Datei für diese Filmsuch -App
// styles.scss
html ,
body {
height : 100 % ;
font-family : Arial , sans-serif ;
margin : 0 ;
background-color : #303030 ;
}
.container {
color : #fff ;
min-height : 100 % ;
margin-bottom : -50 px ;
margin : 0 auto ;
max-width : 1380 px ;
.row {
display : flex ;
flex-direction : row ;
flex-wrap : wrap ;
width : 100 % ;
justify-content : center ;
.col {
display : flex ;
flex-direction : column ;
flex : 0 0 13 % ;
width : 100 % ;
color : #fff ;
margin-bottom : 5 px ;
position : relative ;
.item {
.bg {
background-size : cover !important ;
background-repeat : no-repeat !important ;
background-position : center !important ;
position : relative ;
height : 250 px ;
display : block ;
}
}
}
.col.dn {
display : none ;
}
}
.row.wrapper {
max-width : 1200 px ;
margin : 0 auto
}
}
@media ( max-width : 900 px ) {
.container .row .col {
flex : 0 0 20 % ;
}
}
@media ( max-width : 500 px ) {
.container .row .col {
flex : 0 0 33 % ;
}
}
Wenn Sie diese App kostenlos hosten möchten, besuchen Sie sie jederzeit, wenn Sie möchten, und teilen Sie sie mit anderen, und befolgen Sie die folgenden Schritte.
package.json
für Heroku hinzuline 6
Hinzufügen "start:heroku": "node dist/server/main.js",
line 7
Hinzufügen "heroku-postbuild": "npm run build:ssr"
Procfile
hinzu.touch Procfile
Fügen Sie diese Zeile hinzu. web: npm run start:heroku
in der Datei.server.ts
process.env.TOKEN
line 20
add const apiKey = process.env.TOKEN;
git commit -am "make a commit."
dann git push
Heroku CLI
Login bei Heroku vom Terminal Run: heroku login
.heroku create angular-movie-search
und git push heroku master
.key: TOKEN
und value: TMDB api key
.Wenn der von Ihnen erstellte Heroku -App -Name genommen wird, machen Sie einen eindeutigen Namen, der verfügbar ist. Ich werde einen Teil 2 für dieses Tutrien hinzufügen, damit wir weitere Filmdaten anzeigen und die Seite interaktiv machen können, indem wir Film -Trailer laden. Danke fürs Lesen. Voller Quellcode
angular cli
.Zeigen wir einige Informationen über den Film, wenn wir über das Filmbild schweben. Die Suchnutzlast enthält eine Filmbewertung von 0 - 10. Das Rating (VEMAIL_AVERSE) kann in ein Array umgewandelt werden, um die Bewertung als Sternsymbole in Höhe der Länge des Arrays anzuzeigen.
Die payload
sind die Daten, die nach einer Suchanfrage an die api
zurückgesendet wurden. Sie erhalten eine maximale Ergebnisse von 20 Ergebnissen pro Antwort. Je kleiner die Nutzlast, desto schneller werden die Daten in der Benutzeroberfläche gerendert. Dies ist ein Beispiel für das erste Objekt eines von der API zurückgesandten Oserverables.
{
"popularity" : 24.087 ,
"id" : 670466 ,
"video" : false ,
"vote_count" : 29 ,
"vote_average" : 6.8 ,
"title" : " My Valentine " ,
"release_date" : " 2020-02-07 " ,
"original_language" : " en " ,
"original_title" : " My Valentine " ,
"genre_ids" :[ 53 , 27 ],
"backdrop_path" : " /jNN5s79gjy4D3sJNxjQvymXPs9d.jpg " ,
"adult" : false ,
"overview" : " A pop singer's artistic identity is stolen by her ex-boyfriend/manager and shamelessly pasted onto his new girlfriend/protégé. Locked together late one night in a concert venue, the three reconcile emotional abuses of the past . . . until things turn violent. " , "poster_path" : " /mkRShxUNjeC8wzhUEJoFUUZ6gS8.jpg "
}
Im ersten Teil dieses Tutorials habe ich das poster_path
verwendet, um das Filmbild anzuzeigen, und jetzt kann ich mit dem vote_average
die Bewertung des Films verwenden. Ich habe eine Funktion innerhalb des Controllers der Komponente erstellt, um die Bewertung in ein Array umzuwandeln, das dann den Wert der vote_average
-durchschnittlich auf eine ganze Zahl darstellen kann und Goldstars verwendet, um die Bewertung zu repräsentieren, wenn ich über das Filmbild schwebe.
< span class =" star-rating " *ngFor =" let star of rating(item) " >
< span > ☆ </ span >
</ span >
//home.component.ts line 53
public rating ( movie ) {
let rating = Array ( Math . round ( movie . vote_average ) ) . fill ( 0 ) ;
movie . rating = rating ;
return rating ;
}
Stylen Sie dann den Inhalt für den zurückgegebenen Wert der Bewertung, damit wir nur die Sterne auf dem Schwebedruck sehen. Vollständige SCSS -Datei für die Dialogkomponente
// components/dialog/dialog.component.scss
.item .bg .info {
background-color : rgba ( 0 , 0 , 0 , 0.0 );
position : absolute ;
bottom : 0 ;
color : #fff ;
font-size : 9 px ;
text-transform : uppercase ;
width : 100 % ;
transition : linear .3 s ;
p {
opacity : 0 ;
transition : linear .3 s ;
}
.star-rating {
color : transparent ;
span :before {
content : " 2605 " ;
position : absolute ;
color : gold ;
}
}
}
.item .bg .item :hover {
.info {
background-color : rgba ( 0 , 0 , 0 , 0.5 );
p {
color : #fff ;
opacity : 1 ;
font-size : 14 px ;
}
}
}
Als nächstes füge ich einen Link hinzu, der eine weitere API-Anfrage für einen Film-Trailer (Vorschau) sendet und dann einen Dialog (Popup-Fenster) öffnen, um den Trailer mit der Übersicht (Beschreibung) des Films anzuzeigen.
Wenn ich die Filmversuchsinformationen aus der API abfahre, möchte ich den Medienlink in einen HTML <iframe>
einbetten. Ich möchte einen "Trailer" -Link hinzufügen, der ein Fenster öffnet, um den Trailer beim Klicken anzuzeigen. Mit angular cli
erzeugt ich eine neue Dialogkomponente. Im Terminaltyp ng gc components/dialog --module=app.module.ts
. In diesem Befehl wird die Komponente automatisch zu app.module.ts
hinzugefügt.
Um ein Pop-up-Fenster von Grund auf neu zu erstellen, muss ich ein wenig CSS und einige Winkeltricks verwenden, um mir eine spezielle class
hinzuzufügen, wenn der "Trailer" -Link geklickt wird. Die dialog component
verwendet einen boolean
um einer div
eine active
Klasse hinzuzufügen, um einen dunklen Überlagerungshintergrund mit einem Popup-Fenster anzuzeigen, das in der Mitte des Overlays positioniert ist. Verwenden einer Winkelrichtlinie [ngClass]
Wenn isOpen
boolean
true
ist, fügen Sie der Overlay id
active
Klasse hinzu. <div id="overlay" [ngClass]="{'active': isOpen}">
Dadurch kann ich das Overlay div
ausblenden, bis es active
ist, wenn ich auf den Trailer -Link klicke und isOpen
gleich wahr mache. Ich muss nur einige Inputs
zur app-dialog
Komponente hinzufügen.
<!-- home.component.html line 1 -->
< app-dialog
[isOpen] =" isOpen "
[selectedMovie] =" selectedMovie "
[trailerUrl] =" trailerUrl "
(close) =" isOpen = false " > </ app-dialog >
//dialog.component.ts
import { Component , Input , Output , EventEmitter } from '@angular/core' ;
@ Component ( {
selector : 'app-dialog' ,
templateUrl : './dialog.component.html' ,
styleUrls : [ './dialog.component.scss' ]
} )
export class DialogComponent {
@ Input ( 'selectedMovie' )
public selectedMovie : any ;
@ Input ( 'trailerUrl' )
public trailerUrl : any ;
@ Input ( 'isOpen' )
public isOpen : boolean ;
@ Output ( ) close = new EventEmitter ( ) ;
constructor ( ) { }
public closeModal ( ) {
this . isOpen = false ;
this . selectedMovie = null ;
this . trailerUrl = null ;
this . close . emit ( ) ;
}
}
Ich verwende Input
, um Daten aus dem home.component
auf Klicken zu injizieren, indem ich eine function
aufruft, und ich verwende Output
um eine function
emit
, wenn das Dialogfeld geschlossen ist. Das Dialogfeld kann geschlossen werden, indem Sie oben rechts auf das X klicken oder auf das Overlay klicken. Die closeModal()
-Funktion entfernen die active
Klasse, setzt alle Werte zurück und gibt eine Funktion aus, um isOpen
in home.component
zurückzusetzen.
<!-- dialog.component.html -->
< div id =" overlay " [ngClass] =" {'active': isOpen} " (click) =" closeModal() " >
< div class =" modal " (click) =" $event.stopPropagation() " >
< div class =" modal-header " >
< span class =" header-title " *ngIf =" selectedMovie != null " >
{{selectedMovie?.title ? selectedMovie?.title : selectedMovie?.name}}
({{selectedMovie?.release_date ? (selectedMovie?.release_date | date: 'y') : selectedMovie?.first_air_date | date: 'y'}})
</ span >
< span class =" right " (click) =" closeModal() " > X </ span >
</ div >
< div class =" content " >
< div id =" top " class =" row " >
< div *ngIf =" trailerUrl != null " class =" col-trailer " >
< iframe [src] =" trailerUrl " width =" 560 " height =" 315 " frameborder =" 0 " allowfullscreen > </ iframe >
</ div >
< div class =" col-overview " >
< span class =" star-rating " *ngFor =" let star of selectedMovie?.rating " >
< span > ☆ </ span >
</ span >
< span >
{{selectedMovie?.rating?.length}}/10
</ span >
< br >
< hr >
< span > {{selectedMovie?.overview}} </ span > < br >
</ div >
</ div >
</ div >
</ div >
</ div >
trailerUrl
und werden Sie in einem <iframe>
angezeigt. Der selectedMovie
wird vom home.component
weitergegeben, wenn der Filmlink geklickt wird. Bevor das Dialog geöffnet wird, muss ich den Filmtrailer aus der API abrufen. Ich habe der api.ts
-Datei einen neuen API -Anruf hinzugefügt.
//home.component.ts
//line 17
public isOpen: boolean = false ;
public selectedMovie: any ;
public trailerUrl: any ;
//line 37
public openTrailer ( movie ) {
this . selectedMovie = movie ;
this . dataService . trailer ( movie . id ) . subscribe ( res => {
if ( res [ 0 ] != null ) {
if ( res [ 0 ] . site === 'YouTube' ) {
this . trailerUrl = this . sanitizer . bypassSecurityTrustResourceUrl (
`https://www.youtube.com/embed/ ${ res [ 0 ] . key } `
) ;
this . isOpen = true ;
}
}
} )
}
Von hier aus funktioniert der data.service
als mittlerer Manager, um mit dem server / api
zu sprechen und die Antwort an den Client zurückzugeben (Front-End). Der erste Index in der Antwort mit Usaly ist ein Link zu YouTube, in dem sich fast alle Filmanhänger befinden, sodass ich eine Bedingung verwende, um speziell nur YouTube -Anhänger zu verwenden und den Trailer nicht zu öffnen. Zum Spaß können Sie diesen Zustand hinzufügen, wenn Sie den Trailer von einer anderen Videoquelle öffnen möchten.
//data.service.ts line 24
trailer ( item ) {
let searchterm = `query= ${ item } ` ;
try {
this . result = this . http . post ( '/trailer' , searchterm , { headers } ) ;
return this . result ;
} catch ( e ) {
console . log ( e , 'error' )
}
}
Ich benutze try catch
um einen Fehler zu bewältigen, aber es gibt viele Möglichkeiten, einen Fehler in angular
zu bewältigen. Dies war nur der Einfachheit auf meinem Ende.
//server.ts line 42
server . post ( '/trailer' , async ( req , res ) => {
let searchquery = req . body . query ;
let encsearchquery = encodeURIComponent ( searchquery ) ;
const data = await api . data . trailer ( encsearchquery , apiKey ) ;
res . status ( 200 ) . json ( data ) ;
} )
Ich verwende eine async function
, die auf die API await
, um uns die Nutzlast (Antwort) zu geben, bevor der post
abgeschlossen wird, um einen Serverfehler zu vermeiden.
//api/api.ts
//line 4
let trailerInfo = [ ] ;
//line 19
methods . trailer = async ( id : string , apiKey : string ) => {
let apiUrl = `https://api.themoviedb.org/3/movie/ ${ id } /videos?api_key= ${ apiKey } &language=en-US` ;
let trailerPromise = new Promise ( ( resolve , reject ) => {
request ( apiUrl , { } , function ( err , res , body ) {
let data = JSON . parse ( body ) ;
trailerInfo = data [ 'results' ] ;
resolve ( ) ;
} ) ;
} ) ;
let result = await trailerPromise ;
return trailerInfo ;
} ;
Ich benutze diesen TMDB-Endpunkt https://api.themoviedb.org/3/movie/${id}/videos?api_key=${apiKey}&language=en-US
api_key=$ apikey arteschwage=en-us, um den Filmtrailer per Film- id
zu erhalten. Die id
und apikey
werden unter Verwendung von Typscript -Klammern und Backticks in den Endpunkt übergeben, was eine neue Möglichkeit ist, mit JS dynamische Werte hinzuzufügen, und es sieht viel schöner aus als ein +
, um Werte zu verkettet.
Wenn die Daten auf die YouTube-Bedingung erfüllen, wird das Diglog-Popup geöffnet und die Daten werden in der HTML und den angular
interpolierten Zeichenfolgen {{selectedMovie.title}}
angezeigt, verarbeitet die Doppelklammern die Daten in der HTML dynamisch.
Etwas, über das nicht immer mit Projekten wie diesem gesprochen wird, ist, dass es nicht viel Zeit in Anspruch nehmen würde, dies in ein völlig anderes Projekt umzuwandeln. Sie können die Endpunkte in der Datei api.ts
leicht ändern, um mit einer anderen API zu kommunizieren und verschiedene Daten in der Benutzeroberfläche anzuzeigen. Natürlich müssten Sie einige der Variablen ändern, die Konventionen benennen, damit der Code sinnvoll ist, aber dieses Projekt kann mit etwas anderem recycelt werden, an dem Sie sich möglicherweise mehr interessieren. Sehen Sie sie als Vorlage an API-Datei, um alle Daten zu verarbeiten, die Sie abholen möchten, und zum Front-End zur Anzeige zurücksenden. Ändern Sie den Header -Titel in home.html
in so etwas wie Job Search
und stellen Sie eine Verbindung zu einer Joblisten -API, mit der Jobs beispielsweise nach Schlüsselwörtern abrufen können. Sobald Sie gestartet sind, ist alles möglich. Danke, dass du mit mir codiert hast. Viel Glück. Voller Quellcode
Randnotiz: Ich habe gerade in dieser Minute richtig herausgefunden, dass ein html5
-Dialog tag
<dialog open>This is an open dialog window</dialog>
Es mag etwas zu neu sein und einen fehlenden Browserunterstützung haben, aber vielleicht können Sie kreative Entwickler einen Weg finden, dies zu verwenden, anstatt dass ich es von Grund auf neu "mach es von Grund auf".
Die TMDB -API hat kürzlich einen neuen Endpunkt zu ihrer API hinzugefügt, der Ihnen mitteilt, wo ein Film mit der ID eines Films streamen. Ich habe auch einen weiteren Endpunkt hinzugefügt, um die Besetzung der Filme in der Suchnutzlast abzurufen. Ich habe die api.ts
-Datei aktualisiert, um diese neuen Anfragen zu bearbeiten.
Nachdem ich die Suchergebnisse erhalten habe, verwende ich die RXJS forkJoin
-Methode und map
um mehrere Anfragen für providers
für jeden Film per id
zu stellen. Die Anbieterdaten werden in das mvProvider
-Array pushed
und dann dem searchInfo
-Objekt hinzugefügt, das dann mit Film -Ergebnis -Array, Anbietern Array und Credits Array an den Client zurückgesendet wird.
Abhängig von der Menge an Suchergebnissen kann eine einzige Suchanforderung bis zu 30 API -Anforderungen generieren, bevor die Daten behoben und an den Client gesendet werden. Ich habe eine sleep
erstellt, um auf alle Anfragen zu await
, bevor eine resolve
imComplete-Daten an das Front-End zurücksendet. Es ist nicht der beste Weg, um damit umzugehen, aber es funktioniert. Die Verwendung von async
und await
ist wichtig, um sicherzustellen, dass alle Daten abgeholt werden, bevor das promise
resolved
wird.
//api.ts
let request = require ( 'request' )
let methods : any = { }
let searchInfo = [
{
results : [ ] ,
providers : [ ] ,
credits : [ ]
}
]
import { forkJoin } from 'rxjs'
methods . search = async ( term : string , apiKey : string ) => {
let type = 'movie'
let searchQuery = `https://api.themoviedb.org/3/search/ ${ type } ?api_key= ${ apiKey } &language=en-US&page=1&include_adult=false&query= ${ term } `
let searchPromise = new Promise ( ( resolve , reject ) => {
request ( searchQuery , { } , function ( err , res , body ) {
let mvProviders = [ ]
let mvCredits = [ ]
const sleep = ( ms ) => new Promise ( resolve => setTimeout ( resolve , ms ) )
let data = JSON . parse ( body )
searchInfo [ 0 ] [ 'results' ] = data [ 'results' ]
const providers = async ( ) => {
forkJoin (
data [ 'results' ] . map ( m =>
request (
`https://api.themoviedb.org/3/ ${ type } / ${ m . id } /watch/providers?api_key= ${ apiKey } ` ,
{ } ,
async function ( err , res , body ) {
let data = await JSON . parse ( body ) ;
mvProviders . push ( data ) ;
if ( mvProviders . length > 1 ) {
return searchInfo [ 0 ] [ 'providers' ] = mvProviders ;
}
}
)
)
)
}
const credits = async ( ) => {
await providers ( )
forkJoin (
data [ 'results' ] . map ( m =>
request (
`https://api.themoviedb.org/3/ ${ type } / ${ m . id } /credits?api_key= ${ apiKey } &language=en-US` ,
{ } ,
async function ( err , res , body ) {
let data = await JSON . parse ( body ) ;
mvCredits . push ( data ) ;
if ( mvCredits . length > 1 ) {
searchInfo [ 0 ] [ 'credits' ] = mvCredits ;
}
}
)
)
) ;
await sleep ( 1500 ) ;
resolve ( 'done' ) ;
}
credits ( )
} ) ;
} ) ;
let result = await searchPromise ;
return searchInfo ;
}
export const api = { data : methods } ;
Ich musste die Suchmethode aus den peviösen Tutorials neu aufstellen, da sie mich davon abhielt, mehr verwandte Filmdaten in das Array hinzuzufügen, bevor ich sie in der Datei home.component.html
präsentierte.
In der html
-Datei kann ich Angular
Ereignisbindung (input)
verwenden, um eine Ereignisänderung zu hören und eine Funktion aufzurufen, die im Ereignis besteht, das den Suchtext enthält.
Ich kann den Suchtext an die api.ts
-Datei übergeben, die ihn dem api endpoint
angehängt und die Daten request
.
Wenn der req
(Anfrage) erfolgreich ist, wird die Nutzlast Res (Antwort) an den Client (Front-End) zurückgesandt, und ich kann die Daten mit Logik in home.component.ts
(Controller) formatieren. Ich habe ein array
namens results
erstellt, um die formatierten Daten zu definieren, damit die Schauspieler und der Streaming -Dienst in der Benutzeroberfläche angezeigt werden.
<!-- home.component.html -->
< div class =" row " >
< input (input) =" doSearch($event) " type =" search " class =" form-control " placeholder =" search " >
< span class =" search-title " > {{ results?.length ? 'Results' : 'Search' }} </ span >
</ div >
// home.component.ts
...
ngOnInit ( ) : void {
}
public doSearch ( e ) {
if ( e . target . value . length > 2 ) {
this . dataService . search ( e . target . value ) . pipe (
debounceTime ( 400 ) ) . subscribe ( res => {
this . format ( res )
} )
}
if ( e . target . value . length == 0 ) {
this . results = [ ]
this . selectedMovie = null
}
}
public format ( data ) {
this . util . relatedInfo ( data [ 0 ] . results , data [ 0 ] . providers , 'providers' , 'movies' )
this . util . relatedInfo ( data [ 0 ] . results , data [ 0 ] . credits , 'credits' , 'movies' )
this . loading = false
this . results = data [ 0 ] . results
console . log ( this . results , 'res' )
}
...
Gehen Sie zur Datei full home.component.ts
.
Ich muss die Daten formatieren, um providers
und credits
mit Filmen zu entsprechen, denen sie durch ID angehören. Ich habe einen service
für die Formatierung erstellt. Durch die Verwendung des Befehls ng gs services/util
kann ich eine Servicedatei mit Angular cli
generieren. Die Funktionen in einem service
können von allen components
einer Winkel -App verwendet werden, indem der service
in den component
importiert wird. Jetzt kann ich die relatedInfo
-Funktion in home.component.ts
aufrufen.
//util.service.ts
public relatedInfo ( items , extras , type : string , itemType : string ) {
for ( let item of items ) {
for ( let e of extras ) {
if ( item . id === e . id ) {
if ( type === 'credits' ) {
item . credits = e ;
item . type = itemType ;
if ( e . cast [ 0 ] != null ) {
item . credit1 = e . cast [ 0 ] [ 'name' ] ;
item . credit1Pic = e . cast [ 0 ] [ 'profile_path' ] ;
item . credit1Char = e . cast [ 0 ] [ 'character' ] ;
}
if ( e . cast [ 1 ] != null ) {
item . credit2 = e . cast [ 1 ] [ 'name' ] ;
item . credit2Pic = e . cast [ 1 ] [ 'profile_path' ] ;
item . credit2Char = e . cast [ 1 ] [ 'character' ] ;
}
}
if ( type === 'providers' ) {
item . provider = e [ 'results' ] . US != null ? e [ 'results' ] . US : 'unknown' ;
}
}
}
}
return items ;
}
Ich muss die Credits in der Suchansicht und in der Trailer -Ansicht anzeigen. Da die HTML bereits für das results
eingerichtet ist, muss ich einige Anpassungen in home.component.html
und dialog.component.html
vornehmen, um die schauspielerischen Credits des Films anzuzeigen.
<!-- home.component.html line 21 -->
...
< span class =" info " >
< p >
< span *ngIf =" item?.credit1 " > Cast: {{item?.credit1}}, {{item?.credit2}} < br > </ span >
< span class =" star-rating " *ngFor =" let star of rating(item) " > < span > ☆ </ span > </ span > < br >
< span class =" trailer-link " (click) =" openTrailer(item) " > Trailer </ span >
</ p >
</ span >
...
<!-- dialog.component.html line 24 -->
...
< span > {{selectedMovie?.overview}} </ span > < br >
< span class =" row cast " *ngIf =" selectedMovie?.credits " >
< span class =" col-cast " *ngFor =" let actor of selectedMovie?.credits?.cast; let i = index " >
< span [ngClass] =" {'dn': i > 3 || actor?.profile_path == null, 'cast-item': i <= 3 && actor?.profile_path != null} " >
< img src =" https://image.tmdb.org/t/p/w276_and_h350_face{{actor?.profile_path}} " alt =" image of {{actor?.name}} " > < br >
{{actor?.name}} < br > ({{actor?.character}})
</ span >
</ span >
</ span >
...
Ich habe einige neue CSS -Stile hinzugefügt, um diese neuen Daten zu behandeln.
/* dialog.component.scss line 111 */
...
.row.cast {
margin-top : 20 px ;
.col-cast {
flex : 0 0 50 % ;
text-align : left ;
font-size : 10 px ;
img {
width : 30 % ;
border : 1 px solid #fff ;
border-radius : 8 px ;
}
.cast-item {
padding-bottom : 5 px ;
display : block ;
}
.dn {
display : none ;
}
}
}
...
Full dialog.component.scss
hier.
Um den Streaming -Dienst jedes Films in der zurückgegebenen Reihe von Filmen anzuzeigen, verwende ich eine function
namens getProvider
. Diese Funktion nimmt ein Argument und für jede Schleife durch die Anbieterdaten und definiert eine kurze string
für die Anzeige. Einige Anbieter -Titel sind für die Benutzeroberfläche zu groß und müssen mit if / else
Conditonals geändert werden.
<!-- home.component.html line 28 -->
< span *ngIf =" item?.provider != null " class =" {{getProvider(item?.provider)}} probar " >
{{getProvider(item?.provider)}}
</ span >
// home.component.ts line 75
...
public getProvider ( pro ) {
try {
if ( pro === 'unknown' ) {
return ''
} else if ( pro [ 'flatrate' ] != null ) {
if ( pro . flatrate [ 0 ] . provider_name === 'Amazon Prime Video' ) {
return 'prime'
} else if ( pro . flatrate [ 0 ] . provider_name . toUpperCase ( ) === 'IMDB TV AMAZON CHANNEL' ) {
return 'imdb tv'
} else if ( pro . flatrate [ 0 ] . provider_name . toUpperCase ( ) === 'APPLE TV PLUS' ) {
return 'apple tv'
} else {
return pro . flatrate [ 0 ] . provider_name . toLowerCase ( )
}
} else if ( pro [ 'buy' ] != null ) {
return ''
} else if ( pro [ 'rent' ] != null ) {
return ''
} else {
return ''
}
} catch ( e ) {
console . log ( e , 'error' )
}
}
...
Verwenden Sie nun die Probar class
die ich den Streaming -Dienstanbieter stylen kann. Ich kann den Anbieternamen auch in der HTML -Klasse verwenden, indem ich die Curly -Klammern der Winkelinterpolation verwendet, um dem Probar -Stil eine dynamische Hintergrundfarbe hinzuzufügen.
/* styles.scss line 58 */
...
.probar {
position : absolute ;
bottom : 0 ;
width : 100 % ;
text-align : center ;
font-size : 14 px ;
font-weight : 300 ;
letter-spacing : 2 px ;
opacity : 0.8 ;
color : #fff ;
text-transform : uppercase ;
background : #555 ;
}
.netflix {
background : #e00713 ;
}
.hbo {
background : #8440C1 ;
}
.prime {
background : #00A3DB ;
}
.hulu {
background : #1DE883 ;
}
.disney {
background : #111D4E ;
}
.apple {
background : #000 ;
}
...
Ich beschließe, den Trailer -Modal die volle Breite des Bildschirms zu machen und ein Hintergrundbild des ausgewählten Films hinzuzufügen. Ich habe dialog.component.html
aktualisiert, indem ich die Angular -Anweisung angular [ngStyle]
verwendete, um das Film -Hintergrundbild zu verarbeiten. Ich habe eine Klasse namens full
in die modal
hinzugefügt, dann habe ich der aktiven modal.full
-Klasse neue Stile hinzugefügt.
<!-- dialog.component.html line 11 -->
...
< div id =" top " class =" row " *ngIf =" isOpen " [ngStyle] =" {'background': 'linear-gradient(-225deg, rgba(0,0,0,0.6) 50%, rgba(0,0,0,0.6) 80%), url(https://image.tmdb.org/t/p/w533_and_h300_bestv2/'+ selectedMovie?.backdrop_path +')' } " >
...
/* dialog.component.scss line 133 */
...
.modal.full {
height : 100 % ;
min-width : 100 % ;
#top {
height : 500 px ;
background-size : cover !important ;
background-repeat : no-repeat !important ;
}
.row .col {
flex : 0 0 11 % ;
}
.content {
.col-trailer {
align-self : center ;
text-align : center ;
}
.col-overview {
align-self : center ;
}
}
}
...
Dies ist eine coole Funktion, die ich häufig benutze, um herauszufinden, wo ein Film streamen und die Suche schnell und schneller reagiert als eine Google -Suche, denke ich.
Eine unterhaltsame Herausforderung für fortgeschrittene Entwickler, die dies lesen, wäre, wenn Sie den api.ts
-Code neu aufstellen könnten. Ich verwende verschachtelte Promises
, auf eine resolve
zu warten, bevor ich die Filmdaten an den Kunden zurücksende. Allerdings mag ich nicht, wie ich eine Schlafungsfunktion await
sleep
verwenden musste, damit die Daten auf die credit
und provider
warten, bevor die Filmdaten an den client
zurückgesendet wurden. Ich sollte die sleep
nicht verwenden müssen, die nur ein Patch war function
um mir zu helfen. Das Problem war, dass ich zu viele Bereiche erstellt habe und die Entschlossenheit nicht so funktionierten, wie ich es wollte. Wenn jemand die Entschlossenheit zur Arbeit bekommen kann, ohne die sleep
Timer function
zu verwenden, wäre ich sehr beeindruckt. Danke fürs Lesen!