يتكون هذا التطبيق من Angular (الإصدار 12.2.8). هذا تطبيق عرض من جانب الخادم يستخدم node.js
و express
ويبحث عن عناوين الأفلام. يتم استضافة تطبيق الصفحة الفردية مجانًا على Heroku (منصة التطبيق السحابية). ستحتاج إلى إنشاء حساب مجاني مع themoviedb.org للمشاركة مع هذا البرنامج التعليمي. فيما يلي تعليمات كيفية شراء مفتاح API TMDB.
تم تثبيته محليًا بواسطة 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
Change line 12
to "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
في استخدام FormControl
على إدخال HTML وعندما تتغير القيمة (النص في إدخال البحث) ، سيتم إرسال طلب API إلى الخادم.
//app.module.ts
import { HttpClientModule } from "@angular/common/http" ;
import { ReactiveFormsModule , FormsModule } from "@angular/forms" ;
FormControl
في home.component.html
.async Observable
function
لإطلاق Oninit. <!-- 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
في تطبيقك.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 >
هناك بعض الأشياء التي يجب تفريغها في واجهة المستخدم هذه. أنا أستخدم توصيلًا ثلاثيًا داخل شريحة الاستيفاء الزاوي لعرض النص "البحث" أو "النتائج" على ما إذا كانت هناك بيانات لإظهارها.
{{ (results | async)?.length ? 'Results' : 'Search' }}
أنا أستخدم التوجيه [ngClass]
الذي هو بصرف النظر عن الإطار الزاوي. أقوم بإضافة الفئة dn
إذا كان poster_path
البيانات null
ثم في styles.scss
ADD .dn {display: none;}
لتجنب عناصر الفيلم الفارغة. أنا أيضًا أستخدم التوجيه [ngStyle]
لإضافة صورة الخلفية لصورة ملصق كل عنصر فيلم ديناميكيًا.
لقد أضفت بعض أنماط 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
لـ Herokuline 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 من Terminal Run: heroku login
.heroku create angular-movie-search
و git push heroku master
.key: TOKEN
والقيمة value: TMDB api key
.إذا تم أخذ اسم تطبيق Heroku الذي قمت بإنشائه بتكوين اسم فريد متاح. سأضيف جزءًا 2 لهذا tutrial حتى نتمكن من إظهار المزيد من بيانات الأفلام وجعل الصفحة تفاعلية من خلال تحميل مقطورات الأفلام. شكرا لك على القراءة. رمز المصدر بالكامل
angular cli
.دعنا نعرض بعض المعلومات حول الفيلم عندما نحوم على صورة الفيلم. يوفر حمولة البحث درجة تصنيف الأفلام من 0 إلى 10. يمكن تحويل التصنيف (Vote_average) إلى صفيف لإظهار التصنيف على أنه أيقونات النجوم مساوية لطول الصفيف.
payload
هي البيانات المرسلة بعد تقديم طلب بحث إلى api
. سوف تحصل على الحد الأقصى من 20 نتيجة لكل استجابة. أصغر حمولة كلما تم تقديم البيانات بشكل أسرع في واجهة المستخدم. هذا مثال على الكائن الأول من Oserverable أرسله API.
{
"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 آخر لمقطورة فيلم (معاينة) ثم افتح مربع حوار (نافذة منبثقة) لعرض المقطورة مع نظرة عامة (الوصف) للفيلم.
عندما أحضر معلومات Trialer Movie من واجهة برمجة التطبيقات ، أريد تضمين رابط الوسائط داخل HTML <iframe>
. أرغب في إضافة رابط "مقطورة" من شأنه أن يفتح نافذة لعرض المقطورة عند النقر عليها. باستخدام angular cli
سأقوم بإنشاء مكون مربع حوار جديد. في نوع Terminal ng gc components/dialog --module=app.module.ts
. سيضيف هذا الأمر المكون إلى app.module.ts
تلقائيًا.
لإنشاء نافذة منبثقة من نقطة الصفر ، أحتاج إلى استخدام القليل من CSS وبعض الحيل الزاوية لمساعدتي في إضافة class
خاصة عند النقر فوق رابط "Trailer". يستخدم dialog component
boolean
لإضافة فئة active
إلى div
لإظهار خلفية تراكب مظلمة مع نافذة منبثقة في وسط التراكب. باستخدام توجيه زاوي [ngClass]
إذا كان isOpen
boolean
true
، أضف فئة active
إلى id
التراكب. <div id="overlay" [ngClass]="{'active': isOpen}">
هذا يسمح لي بإخفاء تراكب div
حتى يكون active
، عندما أنقر على رابط المقطورة وجعل isOpen
متساويًا. كل ما أحتاجه هو إضافة بعض Inputs
إلى مكون app-dialog
.
<!-- 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 ( ) ;
}
}
أنا أستخدم Input
لحقن البيانات من home.component
على النقر من خلال استدعاء function
وأستخدم Output
emit
function
عند إغلاق مربع الحوار. يمكن إغلاق مربع الحوار عن طريق النقر على X في الجزء العلوي الأيمن أو بالنقر فوق التراكب. ستقوم دالة closeModal()
بإزالة الفئة active
، وإعادة ضبط جميع القيم وإبعاد وظيفة لإعادة تعيين isOpen
في home.component
.
<!-- 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
على YouTube وعرضها في <iframe>
. يتم تمرير selectedMovie
من home.component
عند النقر فوق رابط الفيلم ولكن قبل فتح مربع الحوار ، أحتاج إلى جلب مقطورة الفيلم من API. أضفت مكالمة API جديدة إلى ملف api.ts
//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
وإرسال الاستجابة إلى العميل (الواجهة الأمامية). يكون الفهرس الأول في الاستجابة بفضل هو رابط لـ 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 ) ;
} )
أنا أستخدم async function
التي await
واجهة برمجة التطبيقات لمنحنا الحمولة (استجابة) قبل إكمال post
لتجنب خطأ الخادم.
//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
{دي id
يتم تمرير id
و apikey
إلى نقطة النهاية باستخدام قوسين TypeScript و Backticks ، وهي طريقة جديدة لإضافة قيم ديناميكية مع JS وتبدو أجمل بكثير ثم باستخدام A +
to Concatenate.
إذا تلبية البيانات في حالة YouTube ، يتم فتح المنبثقة المنبثقة في DiAglog وستظهر البيانات داخل HTML والسلاسل angular
المحتملة {{selectedMovie.title}}
، تقوم الأقواس المزدوجة بمعالجة البيانات في HTM بشكل ديناميكي.
شيء لا يتم الحديث عنه دائمًا مع مشاريع مثل هذا هو أنه لن يستغرق الكثير من الوقت لتحويل هذا إلى مشروع مختلف تمامًا. يمكنك بسهولة تغيير نقاط النهاية في ملف api.ts
للتواصل مع واجهة برمجة تطبيقات مختلفة والحصول على بيانات مختلفة لإظهارها في واجهة المستخدم. بالطبع ستحتاج إلى تغيير بعض المتغيرات التي تسميها حتى يكون الرمز منطقيًا ولكن يمكن إعادة تدوير هذا المشروع بشيء آخر قد تكون مهتمًا به أكثر. ملف API للتعامل مع أي بيانات ترغب في جلبها وإرسالها إلى الواجهة الأمامية للعرض. قم بتغيير عنوان الرأس في home.html
إلى شيء مثل Job Search
والتواصل مع واجهة برمجة تطبيقات قائمة الوظائف التي يمكن أن تجلب الوظائف بالكلمات الرئيسية على سبيل المثال. بمجرد أن تبدأ أي شيء ممكن. شكرا لك على الترميز معي. حظ سعيد. رمز المصدر بالكامل
ملاحظة جانبية: اكتشفت للتو هذه اللحظة ، هناك tag
مربع حوار html5
<dialog open>This is an open dialog window</dialog>
لكنها لم تنجح بالنسبة لي في Chrome. قد يكون الأمر جديدًا جدًا ويفتقر إلى دعم المتصفح ، لكن ربما يمكنك أن تجد Devs الإبداعية طريقة لاستخدام ذلك بدلاً من نهج "Do As From Frotch".
أضافت API TMDB مؤخرًا نقطة نهاية جديدة إلى واجهة برمجة التطبيقات الخاصة بهم والتي ستخبرك بمكان بث الفيلم باستخدام معرف الفيلم. لقد أضفت أيضًا نقطة نهاية أخرى لجلب طاقم الأفلام في حمولة البحث. قمت بتحديث ملف api.ts
للتعامل مع هذه الطلبات الجديدة.
بعد الحصول على نتائج البحث ، أستخدم طريقة RXJS forkJoin
map
لتقديم طلبات متعددة providers
لكل فيلم بواسطة id
. يتم pushed
بيانات الموفر إلى صفيف mvProvider
ومن ثم يتم إضافته إلى كائن searchInfo
الذي يتم إرساله بعد ذلك إلى العميل مع صفيف نتائج الأفلام ، وصفيف مقدمي الخدمات ومجموعة الاعتمادات.
اعتمادًا على مقدار نتائج البحث ، يمكن لطلب بحث واحد إنشاء ما يصل إلى 30 طلب API قبل حل البيانات وإرسالها إلى العميل. لقد قمت بإنشاء وظيفة sleep
await
جميع طلبات الانتهاء قبل أن ترسل resolve
البيانات ImComplete إلى الواجهة الأمامية. إنها ليست أفضل طريقة للتعامل مع هذا ولكنها تعمل. من المهم استخدام async
await
للتأكد من اكتمال جميع البيانات التي يتم جلبها قبل resolved
promise
.
//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 } ;
اضطررت إلى إعادة تشكيل طريقة البحث من البرامج التعليمية Pevious لأنها كانت تمنعني من إضافة المزيد من بيانات الأفلام ذات الصلة إلى المصفوفة قبل تقديمها في ملف home.component.html
.
في ملف html
يمكنني استخدام ربط الحدث Angular
(input)
للاستماع لتغيير الحدث واستدعاء وظيفة تمر في الحدث الذي يحتوي على نص البحث.
يمكنني تمرير نص البحث إلى ملف api.ts
إلحاقه إلى api endpoint
request
البيانات.
إذا نجح req
(الطلب) ، يتم إرسال RES (استجابة) الحمولة إلى العميل (الواجهة الأمامية) ويمكنني تنسيق البيانات مع المنطق داخل home.component.ts
(وحدة التحكم). لقد قمت بإنشاء array
تسمى results
لتحديد البيانات المنسقة بحيث تظهر الجهات الفاعلة وخدمة البث في واجهة المستخدم.
<!-- 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' )
}
...
انتقل إلى ملف home.component.ts
الكامل.
أحتاج إلى تنسيق البيانات لمطابقة providers
credits
مع الأفلام التي ينتمون إليها بواسطة ID. لقد قمت بإنشاء service
فائدة للتعامل مع التنسيق. باستخدام الأمر ng gs services/util
يمكنني إنشاء ملف خدمة مع Angular cli
. يمكن استخدام الوظائف في service
بواسطة جميع components
التطبيق الزاوي عن طريق استيراد service
إلى وحدة تحكم component
. الآن يمكنني الاتصال بوظيفة relatedInfo
في home.component.ts
.
//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
كامل.
لعرض خدمة البث لكل فيلم في مجموعة الأفلام التي تم إرجاعها ، أستخدم function
تسمى getProvider
. ستأخذ هذه الوظيفة وسيطة واحدة ولكل حلقة من خلال بيانات المزود وتحديد string
قصيرة لتناسب الشاشة. بعض عناوين الموفر كبيرة جدًا بالنسبة لواجهة المستخدم ويجب تغييرها باستخدام 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' )
}
}
...
الآن باستخدام class
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 ;
}
...
قررت جعل مقطورة مشروط العرض الكامل للشاشة وإضافة صورة خلفية للفيلم المحدد. لقد قمت بتحديث dialog.component.html
باستخدام التوجيه Angular [ngStyle]
للتعامل مع صورة خلفية الفيلم. أضفت فصلًا يسمى full
إلى فئة modal
، ثم أضفت أنماطًا جديدة إلى فئة 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
قبل إرسال بيانات الفيلم إلى العميل. ومع ذلك ، لا أحب الطريقة التي اضطررت بها لاستخدام وظيفة await
sleep
، بحيث تنتظر البيانات أن يتم إعادة تحديد بيانات credit
provider
قبل إرسال بيانات الفيلم إلى client
. لا ينبغي علي استخدام function
sleep
التي كانت مجرد تصحيح لمساعدتي. كانت المشكلة هي أنني أنشأت عددًا كبيرًا من النطاقات ، ولم تكن الحلول تعمل بالطريقة التي أردت بها. إذا تمكن شخص ما من الحصول على حل العاملات دون استخدام function
مؤقت sleep
، فسوف أكون معجبًا جدًا. شكرا لك على القراءة!