Я работал над SPA с Angular 16, TypeScript и The Movie Database (TMDB).
Я столкнулся с проблемой при отображении трейлеров фильмов в карусели Bootstrap 5.
В app\services\movie-service.service.ts у меня есть:
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';
import { HttpClient } from '@angular/common/http';
import { MovieResponse, Movie } from '../models/Movie';
import { Trailer, TrailerResponse } from '../models/Trailer';
@Injectable({
providedIn: 'root'
})
export class MovieService {
constructor(private http: HttpClient) { }
public getMovieTrailers(id: Number): Observable<TrailerResponse>{
return this.http.get<TrailerResponse>(`${environment.apiUrl}/movie/${id}/videos?api_key=${environment.apiKey}`);
}
public getMovieDetails(id: Number): Observable<Movie>{
return this.http.get<Movie>(`${environment.apiUrl}/movie/${id}?api_key=${environment.apiKey}`);
}
}
В app\models\Trailer.ts у меня есть:
export interface Trailer {
id?: string;
key?: string;
name?: string;
site?: string;
}
export interface TrailerResponse {
results?: Trailer[];
}
Я создал компонент TrailerCarouselComponent со следующим файлом ts:
import { Component, Input } from '@angular/core';
import { Trailer } from '../../models/Trailer';
@Component({
selector: 'app-trailer-carousel',
templateUrl: './trailer-carousel.component.html',
styleUrls: ['./trailer-carousel.component.scss']
})
export class TrailerCarouselComponent {
@Input() trailers!: Trailer[];
}
И этот шаблон:
<div id = "trailersCarousel" class = "carousel slide" data-bs-interval = "false">
<ol *ngIf = "trailers.length > 1" class = "carousel-indicators">
<li
*ngFor = "let video of trailers; let i = index"
data-bs-target = "#trailersCarousel"
attr.data-slide-to = "{{ i }}"
class = "{{ i === 0 ? 'active' : '' }}"
>
{{ i + 1 }}
</li>
</ol>
<div class = "carousel-inner">
<div
*ngFor = "let video of trailers; let i = index"
class = "carousel-item"
[ngClass] = "{ 'active': i === 0 }"
>
<iframe
class = "embed-responsive-item"
src = "https://www.youtube.com/embed/{{ video.key }}"
></iframe>
</div>
</div>
</div>
Я использую указанный выше компонент в компоненте MovieDetailsComponent:
import { Component, Input } from '@angular/core';
import { Movie } from '../../models/Movie';
import { Trailer, TrailerResponse } from '../../models/Trailer';
import { MovieService } from '../../services/movie-service.service';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-movie-details',
templateUrl: './movie-details.component.html',
styleUrls: ['./movie-details.component.scss']
})
export class MovieDetailsComponent {
constructor(private movieService: MovieService, private activatedRoute: ActivatedRoute) {}
public movie!: Movie;
public trailerResponse!: TrailerResponse;
public trailers: Trailer[] | undefined = [];
public getMovieTrailers() {
const movie_id = Number(this.activatedRoute.snapshot.paramMap.get('id'));
this.movieService.getMovieTrailers(movie_id).subscribe((response) => {
this.trailerResponse = response;
this.trailers = this.trailerResponse.results;
if (this.trailers && this.trailers?.length) {
this.trailers = this.trailers.slice(0, 5);
}
});
}
public getMovieDetails() {
const movie_id = Number(this.activatedRoute.snapshot.paramMap.get('id'));
this.movieService.getMovieDetails(movie_id).subscribe((response) => {
this.movie = response;
// get movie trailers
this.getMovieTrailers();
});
}
ngOnInit() {
this.getMovieDetails();
}
}
Шаблон:
<div *ngIf = "trailers && trailers.length" class = "mb-3">
<h2 class = "section-title">Trailers</h2>
<app-trailer-carousel [trailers] = "trailers"></app-trailer-carousel>
</div>
Ошибок компиляции нет, но есть неожиданная (для меня) проблема.
Вместо желаемого результата я получаю эту ошибку в браузере:
NG0904: unsafe value used in a resource URL context
...
at TrailerCarouselComponent_div_3_Template (trailer-carousel.component.html:22:9)
Строка, о которой идет речь, src = "https://www.youtube.com/embed/{{ video.key }} из:
<iframe
class = "embed-responsive-item"
src = "https://www.youtube.com/embed/{{ video.key }}"
></iframe>
Для меня это тем более удивительно, что я уже создал это приложение с помощью Vue3 и Typescript и не столкнулся с проблемой.
Использование трубы, как предложил @Kosta, мне не помогло. Я добавил Stackblitz.



![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


Могут быть небезопасные URL-адреса, если они генерируются динамически, поэтому нам необходимо убедиться, что очищенный доверенный URL-адрес попадает в src, мы можем использовать bypassSecurityTrustResourceUrl из DomSanitizer angular, чтобы гарантировать, что URL-адрес очищен!
HTML
<iframe
class = "embed-responsive-item"
[src] = "trustUrl(video.key)"
></iframe>
тс
constructor(private sanitizer: DomSanitizer) {}
...
...
trustUrl(key: string): string {
return this.sanitizer.bypassSecurityTrustResourceUrl(`https://www.youtube.com/embed/${key}`) as string;
}
...
@RazvanZamfir нам нужно импортировать SecurityContext, можешь попробовать это? он находится в @angular/core
Я сделал это, и iframe содержит всю страницу. Источник <iframe _ngcontent-ng-c2152715611 = "" src = "trustUrl(video.key)" class = "embed-responsive-item"></iframe>. Может быть, <iframe class = "embed-responsive-item" src = "trustUrl(video.key)"></iframe> неправильный?
@RazvanZamfir извините, нам нужно добавить привязку свойства [src] обновил мой ответ!
Я тоже так сделал и получаю ошибку TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'.
@RazvanZamfir обновлено! добавленный тип
Что исправляет ошибку компиляции, так это <iframe *ngIf = "video.key" class = "embed-responsive-item" [src] = "trustUrl(video.key)"></iframe>, но... видео не отображается.
@RazvanZamfir попробуйте bypassSecurityTrustResourceUrl вместо этого обновите ответ демо stackblitz
Я обновил вопрос, указав подробную информацию о родительском компоненте. Пожалуйста, взгляните
Первая проблема заключается в том, что вы передаете не очищенный URL-адрес в iframe, что по умолчанию не разрешено в angular. Создайте канал, который будет обходить безопасность URL-адреса iframe.
ng g p bypass-sanitize
import { Pipe, PipeTransform, inject } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
@Pipe({
name: 'bypassSanitize',
standalone: true,
})
export class BypassSanitizePipe implements PipeTransform {
private readonly domSanitizer = inject(DomSanitizer);
// constructor(private domSanitizier: DomSanitizer) {} old way but still working
transform(value: string) {
return this.domSanitizer.bypassSecurityTrustResourceUrl(value);
}
}
Поскольку у вашего компонента нет свойства Standalone, я предполагаю, что ваше приложение основано на модулях. Вы можете импортировать эту трубу внутрь NgModule.
После того, как вы добавите его в NgModules, давайте воспользуемся этим каналом:
<iframe
class = "embed-responsive-item"
[src] = "'https://www.youtube.com/embed/' + video.key | bypassSanitize"
Предупреждение: вызов этого метода с ненадежными пользовательскими данными подвергает ваше приложение угрозе безопасности XSS!
Ответ на второй вопрос — использовать каналы вместо функций, которые будут напрямую использоваться внутри html, это вызовет многократное обнаружение изменений, лучший подход — каналы или обработка источника через наблюдаемые (поток).
Например:
readonly frameUrl$ = this.SomeStreamForYourFrameUrl$.pipe(
map((url) => this.domSanitizer.bypassSecurityTrustResourceUrl(url)),
);
<iframe
class = "embed-responsive-item"
src = "{{frameUrl$ | async }}"
></iframe>
Имейте в виду, что это более лучший подход async — это канал для прослушивания потоков, поэтому вам не нужно использовать подписку на поток.
Старайтесь чаще использовать сигналы или наблюдаемые (rxjs)
Я понимаю ERROR NullInjectorError: R3InjectorError(AppModule)[BypassSanitizePipe -> BypassSanitizePipe]. Я добавил stackblitz.
Обновление: я добавил BypassSanitizePipe в массив поставщиков в app.module.ts. И у меня все еще есть ошибка: Error: NG0904: unsafe value used in a resource URL context. Взгляните на stackblitz.
Проблема заключалась в том, что я предложил неправильный способ передачи односторонней привязки, это было больше похоже на «взлом», но он отлично подходит для использования rxjs. просто обновите строку передачи src: [src] = "'https://www.youtube.com/embed/' + video.key | bypassSanitize" и все должно работать. Вот stackblitz, имейте в виду, что stackblitz не отображает iframe по соображениям безопасности. Кстати, когда вы импортируете канал, он должен идти в массив объявлений, а не в поставщики, и нет необходимости вводить его в конструктор.
Пожалуйста, попробуйте ответить и на этот вопрос. Спасибо!
Я получаю ошибку
Cannot find name 'SecurityContext'. Did you mean 'isSecureContext'?вapp\components\trailer-carousel\trailer-carousel.component.ts