このアプリケーションはAngularで作成されています(バージョン12.2.8)。これは、 node.js
とexpress
使用し、映画のタイトルを検索するサーバー側のレンダリングアプリです。このシングルページアプリは、Heroku(クラウドアプリケーションプラットフォーム)で無料でホストされています。このチュートリアルに参加するには、themoviedb.orgを使用して無料アカウントを作成する必要があります。 TMDB APIキーを調達する方法の指示は以下にあります。
Angular CLIによってローカルに自動的にインストールされます
ng new movie-search --skip-git
Would you like to add Angular routing? Yes
Which stylesheet format would you like to use? SCSS
このアプリをサーバー側にすることの利点は、アプリケーションが通常のアプリよりも速く画面(UI)に表示できることです。これにより、ユーザーが完全にインタラクティブになる前にアプリケーションレイアウトを表示する機会が得られます。また、SEOにも役立ちます。
ng add @nguniversal/express-engine
server.ts
ファイルが作成されます。package.json
で、json line 12
を"serve:ssr": "node dist/server/main.js",
angular.json
でline 20
"outputPath": "dist/browser",
line 129
から"outputPath": "dist/server",
server.ts
でline 14
をconst distFolder = join(process.cwd(), 'dist/browser');
npm run build:ssr
npm run serve:ssr
ng gc modules/home --module=app.module.ts
homeComponent
を指すルートを作成します。 //app-routing.module.ts
import { HomeComponent } from './modules/home/home.component' ;
const routes : Routes = [
{
path : '' ,
component : HomeComponent
} ,
{
path : '**' ,
component : HomeComponent
}
] ;
app.component.html
プレースホルダーHTMLコンテンツを以下に置き換えます。 <!-- app.component.html -->
< div class =" container " >
< router-outlet > </ router-outlet >
</ div >
このアプリでは、 HttpClinetModule
、 ReactiveFormsModule
、およびFormsModule
app.module.ts
ファイルに追加し、 imports
に追加する必要があります。これにより、このモジュールがアプリ全体を通じて利用可能になります。 HTTPモジュールを使用すると、サーバーに電話をかけることができます。 ReactiveFormsModule
は、HTML入力でFormControl
使用するのに役立ち、値が変更された場合(検索入力のテキスト)API要求がサーバーに送信されます。
//app.module.ts
import { HttpClientModule } from "@angular/common/http" ;
import { ReactiveFormsModule , FormsModule } from "@angular/forms" ;
home.component.html
でFormControl
を使用して検索入力を作成します。async Observable
と発射function
を設定します。 <!-- 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
コマンドをターミナルに書く: ng gs services/data
server.ts
に送信します。 //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' )
}
}
}
server.ts
では、クライアントからのリクエストを処理する関数を作成します。次に、リクエストをTMDBエンドポイントに送信します。 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
フォルダーとapi.ts
ファイルを作成します。mkdir api
では、 cd api
touch api.ts
てAPIディレクトリを設定します。api
ファイルをserver.ts
ファイルにインポートします。 import { api } from './api/api'
。将来的には、 TMDB api
にさまざまなリクエストを追加したい場合はapi.ts
に追加してserver.ts
ファイルを散らかったままにしておくことができます。
//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 } ;
request
ライブラリを使用してAPIリクエストを処理し、応答を解析しています。 TypeScriptでは、エラーのスローを避けるために応答が準備ができているのを待つために約束を使用できます。
settings
をクリックします。API
リンクを見つけて、APIキーを受信するためにアプリの詳細を送信します。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
ファイルにAPIキーを追加します。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 } ) ) ;
...
TMDBエンドポイントからデータ応答を取り戻すと、クライアントに送信されます(フロントエンド)。 home.component.html
は、 async Observable
表示するためにセットアップする必要があります。
<!-- 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 >
このUIで開梱するものがいくつかあります。私は、角度補間ブラケット内の3成分コンディションを使用して、表示するデータがある場合にテキスト「検索」または「結果」を表示しています。
{{ (results | async)?.length ? 'Results' : 'Search' }}
角度フレームワークの別の[ngClass]
指令を使用しています。データposter_path
がnull
ある場合、クラスdn
を追加し、 styles.scss
を追加して.dn {display: none;}
を追加して、空白の映画アイテムを避けます。また、 [ngStyle]
ディレクティブを使用して、各ムービーアイテムのポスター画像の背景画像を動的に追加しています。
Flex Row列のレイアウトで映画の結果を表示するために、いくつかの基本的なCSSスタイルを追加しました。これにより、小さなモバイル画面も処理されます。 scss
ファイルを使用すると、以下に示すようなネストされたCSSを記述できます。この映画検索アプリの完全なSCSSファイル
// 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 % ;
}
}
このアプリを無料でホストしたい場合は、いつでも他の人と共有したいときはいつでもアクセスして、以下の手順に従ってください。
package.json
に追加しますline 6
目に"start:heroku": "node dist/server/main.js",
line 7
目で"heroku-postbuild": "npm run build:ssr"
Procfile
追加します。touch Procfile
このラインを追加しますweb: npm run start:heroku
ファイルに。process.env.TOKEN
に置き換えてからserver.ts
githubとherokuにプッシュする前に。line 20
でconst apiKey = process.env.TOKEN;
git commit -am "make a commit."
その後、 git push
Heroku CLI
がターミナルランからHerokuにログインします: heroku login
。heroku create angular-movie-search
とgit push heroku master
を作成します。key: TOKEN
とvalue: TMDB api key
。作成したHerokuアプリ名が使用されている場合、利用可能な一意の名前を作成します。このTutrialにパート2を追加して、映画の予告編をロードして、さらに映画データを表示し、インタラクティブにすることができます。読んでくれてありがとう。完全なソースコード
angular cli
を使用したダイアログ(モーダル)コンポーネント。映画の画像の上にホバリングするとき、映画に関する情報を示しましょう。検索ペイロードは、映画の評価スコア0-10を提供します。評価(票_average)を配列に変換して、アレイの長さに等しいスターアイコンとして評価を表示できます。
payload
api
に検索リクエストを行った後に送信されたデータです。応答ごとに20の結果が最大になります。ペイロードが小さいほど、データはUIでレンダリングされます。これは、APIによって送信されるOServerableの最初のオブジェクトの例です。
{
"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 "
}
このチュートリアルの最初の部分では、 poster_path
使用して映画の画像を表示しましたが、今ではvote_average
を使用して映画の評価を表示できます。コンポーネントのコントローラー内に関数を作成して、評価をアレイに変換し、それを整数に丸めたvote_average
の値を表し、ゴールドスターを使用して映画の画像の上にホバリングするときの評価を表します。
< 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 ;
}
次に、ホバーに星のみが表示されるように、評価の返された値のコンテンツをスタイリングします。ダイアログコンポーネントの完全なSCSSファイル
// 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 ;
}
}
}
次に、映画の予告編(プレビュー)の別のAPI要求を送信するリンクを追加し、ダイアログ(ポップアップウィンドウ)を開いて、映画の概要(説明)でトレーラーを表示します。
APIから映画のトライアル情報を取得したら、HTML <iframe>
にメディアリンクを埋め込みたいと思います。クリックしたときにトレーラーを表示するためにウィンドウを開く「トレーラー」リンクを追加したい。 angular cli
使用して、新しいダイアログコンポーネントを生成します。ターミナルタイプng gc components/dialog --module=app.module.ts
。このコマンドは、コンポーネントをapp.module.ts
に自動的に追加します。
ゼロからポップアップウィンドウを作成するには、「トレーラー」リンクがクリックされたときに特別なclass
追加するのに役立つ小さなCSSといくつかの角度のトリックを使用する必要があります。 dialog component
boolean
を使用して、 div
にactive
クラスを追加して、オーバーレイの中央に配置されたポップアップウィンドウを備えた暗いオーバーレイの背景を表示します。 Angular Directive [ngClass]
を使用して、 isOpen
boolean
がtrue
場合、オーバーレイid
にactive
クラスを追加します。 <div isOpen
<div id="overlay" [ngClass]="{'active': isOpen}">
これにより、オーバーレイdiv
active
なるまで非表示になります。私がする必要があるのは、 app-dialog
コンポーネントにいくつかのInputs
を追加することです。
<!-- 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 ( ) ;
}
}
Clickを呼び出して、Clickのfunction
からのInput
を使用してhome.component
からデータを注入しており、ダイアログが閉じたときにOutput
使用してfunction
emit
。ダイアログは、右上のXをクリックするか、オーバーレイをクリックして閉じることができます。 closeModal()
関数は、 active
クラスを削除し、すべての値をリセットし、 home.component
でisOpen
をリセットする関数を発します。
<!-- 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
を作成し、 <iframe>
で表示します。 selectedMovie
映画のリンクがクリックされたときにhome.component
から渡されますが、ダイアログが開かれる前に、APIから映画の予告編を取得する必要があります。 api.ts
ファイルに新しいAPI呼び出しを追加しました。
//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 ;
}
}
} )
}
ここからdata.service
は、ミドルマネージャーとして機能してserver / api
と通信し、クライアントに応答を送り返します(フロントエンド)。 USTALLYの応答の最初のインデックスはYouTubeへのリンクです。ここでは、ほぼすべての映画トレーラーが存在するため、YouTubeトレーラーのみを使用するために条件を使用しています。楽しいために、別のビデオソースからトレーラーを開いてもらう場合は、この状態に追加できます。
//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' )
}
}
私はtry catch
使用してエラーを処理していますが、 angular
のエラーを処理する方法はたくさんあります。これは私の側の単純さのためだけでした。
//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 ) ;
} )
サーバーエラーを回避するために、 post
完了する前に、APIがペイロード(応答)を与えるためにAPIをawait
タイプスクリプトasync function
を使用しています。
//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 ;
} ;
私はこのTMDBエンドポイントhttps://api.themoviedb.org/3/movie/${id}/videos?api_key=${apiKey}&language=en-US
api_key_key = {apikey}&language=en-usを映画id
で映画の予告編を取得しています。 id
とapikey
、TypeScriptブラケットとバックテックを使用してエンドポイントに渡されます。これは、JSで動的値を追加する新しい方法であり、A +
使用して値を連結するよりもずっときれいに見えます。
データがYouTubeの条件を満たしている場合、Diaglogポップアップが開かれ、データがHTML内に表示され、 angular
補間文字列{{selectedMovie.title}}
が表示されます。ダブルブラケットはHTMLのデータを動的に処理します。
このようなプロジェクトで常に話しているわけではないことは、これをまったく異なるプロジェクトに変えるのにそれほど時間がかからないということです。 api.ts
ファイルのエンドポイントを簡単に変更して、異なるAPIと通信し、UIに表示する異なるデータを取得できます。もちろん、コードが理にかなっているように命名規則のいくつかの変数を変更する必要がありますが、このプロジェクトはあなたがより興味を持っているかもしれない他の何かでリサイクルできます。フェッチしたいデータを処理するAPIファイルと表示のためにフロントエンドに送り返します。 home.html
のヘッダータイトルをJob Search
ようなものに変更し、たとえばキーワードでジョブを取得できるジョブリストAPIに接続します。始めたら、何かが可能です。私とコーディングしていただきありがとうございます。幸運を。完全なソースコード
サイドノート:私は今すぐにわかりましたhtml5
ダイアログtag
<dialog open>This is an open dialog window</dialog>
が、Chromeではうまくいきませんでした。それは少し新しくてブラウザのサポートが不足しているかもしれませんが、おそらくあなたのクリエイティブな開発者は、私の「ゼロからそれをする」アプローチの代わりにそれを使用する方法を見つけることができます。
TMDB APIは最近、APIに新しいエンドポイントを追加し、映画のIDを使用して映画のストリーミングを示しています。また、検索ペイロードで映画のキャストを取得するために別のエンドポイントを追加しました。 api.ts
ファイルを更新して、これらの新しいリクエストを処理しました。
検索結果を取得した後、RXJS forkJoin
メソッドとmap
使用して、 id
ごとに各映画のproviders
に複数のリクエストを行います。プロバイダーデータはmvProvider
アレイにpushed
、それがsearchInfo
オブジェクトに追加され、その後、映画の結果アレイ、プロバイダーアレイ、クレジットアレイを使用してクライアントに送り返されます。
検索結果の量に応じて、単一の検索要求がデータが解決され、クライアントに送信される前に最大30のAPIリクエストを生成する可能性があります。 resolve
ImCompleteデータをフロントエンドに送り返す前に、すべてのリクエストが終了するためのすべてのリクエストをawait
ためにsleep
機能を作成しました。これを処理する最良の方法ではありませんが、機能します。 async
使用して、 promise
がresolved
前にフェッチされているすべてのデータが完了することをawait
するために重要です。
//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 } ;
home.component.html
ファイルで提示する前に、より関連する映画データをアレイに追加することをブロックしていたため、Pewiousチュートリアルから検索方法をリファクタリングする必要がありました。
html
ファイルでは、 Angular
イベントバインディング(input)
を使用して、イベントの変更を聞き、検索テキストを含むイベントに渡される関数を呼び出すことができます。
検索テキストをapi.ts
ファイルに渡すことができます。API api endpoint
に追加して、データをrequest
。
req
(要求)が成功した場合、ペイロードRES(応答)がクライアント(フロントエンド)に送信され、 home.component.ts
(コントローラー)内のロジックを使用してデータをフォーマットできます。フォーマットされたデータを定義するresults
と呼ばれるarray
を作成して、アクターとストリーミングサービスがUIに表示されるようにします。
<!-- 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' )
}
...
full home.component.ts
ファイルに移動します。
providers
とcredits
IDが所有する映画と一致させるためにデータをフォーマットする必要があります。フォーマットを処理するためのユーティリティservice
を作成しました。コマンドng gs services/util
使用することにより、 Angular cli
を使用してサービスファイルを生成できます。 service
の機能は、 component
コントローラーにservice
をインポートすることにより、Angularアプリのすべてのcomponents
で使用できます。これで、 home.component.ts
でrelatedInfo
関数を呼び出すことができます。
//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 ;
}
検索ビューとトレーラービューにクレジットを表示する必要があります。 HTMLは既にresults
アレイ用に設定されているため、 home.component.html
およびdialog.component.html
で行う必要があるいくつかの調整があります。映画の演技クレジットを表示します。
<!-- 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 >
...
この新しいデータを処理するために、いくつかの新しいCSSスタイルを追加しました。
/* 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 ;
}
}
}
...
完全なdialog.component.scss
はこちら。
返された映画の配列に各映画のストリーミングサービスを表示するために、 getProvider
と呼ばれるfunction
を使用します。この関数は、1つの引数を取り、各ループに対してプロバイダーデータを介してループし、ディスプレイに収まる短いstring
を定義します。一部のプロバイダータイトルはUIには大きすぎて、 if / else
conditonalsを使用して変更する必要があります。
<!-- 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' )
}
}
...
現在、ProBAR class
使用して、ストリーミングサービスプロバイダーをスタイリングできます。また、Angular Anterpolation Curly Bracketsを使用して、Probarスタイルに動的な背景色を追加することにより、HTMLクラスのプロバイダー名を使用することもできます。
/* 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 ;
}
...
トレーラーモーダルを画面の全幅にし、選択した映画の背景画像を追加することにしました。 Angular [ngStyle]
ディレクティブを使用して、映画の背景画像を処理することにより、 dialog.component.html
更新しました。 modal
クラスにfull
という名前のクラスを追加し、Active modal.full
クラスに新しいスタイルを追加しました。
<!-- 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 ;
}
}
}
...
これは、映画がストリーミングされている場所を見つけるために頻繁に使用するクールな機能であり、Google検索よりも速く検索が迅速に応答する場所を見つけます。
これを読んでいる上級開発者にとって楽しい挑戦は、 api.ts
コードをリファクタリングできる場合です。私は、映画データをクライアントに送り返す前に、ネストされたPromises
使用してresolve
を待つことを待っています。ただし、 sleep
await
方法がタイマー機能を待つ方法が好きではありません。これにより、映画のデータがclient
に送信される前に、データがcredit
とprovider
データが再配分されるのを待つようになります。私を助けるために単なるパッチであるsleep
function
を使用する必要はありません。問題は、私があまりにも多くのスコープを作成したことであり、解決は私が望んでいた方法で機能していなかったことでした。誰かがsleep
タイマーfunction
を使用せずに解決することができれば、私は非常に感銘を受けます。読んでくれてありがとう!