Я работаю над SPA с API Vue 3, TypeScript и The Movie Database (TMDB).
Я хочу отобразить постер фильма на странице сведений о фильме, если есть постер, в противном случае я хочу отобразить заполнитель (generic-poster-big.png).
Для этого в src\components\MovieDetails.vue у меня есть:
<template>
<div class = "poster-container text-center text-sm-start my-3">
<img
:src = "genericPoster"
:alt = "movie?.title"
class = "img-fluid shadow-sm"
/>
</div>
</template>
<script lang = "ts">
import { defineComponent } from "vue";
import { useRoute } from "vue-router";
import axios from "axios";
import env from "../env";
import { Movie } from "../models/Movie";
export default defineComponent({
name: "MovieDetails",
data() {
return {
route: useRoute(),
genericPosterBig: require("../assets/generic-poster-big.png"),
movie: null as Movie | null
};
},
mounted() {
this.getMovieDetails();
},
methods: {
getMovieDetails() {
axios
.get(
`${env.api_url}/movie/${this.route.params.id}?api_key=${env.api_key}`
)
.then((response) => {
this.movie = response.data;
})
.catch((err) => console.info(err));
},
computed: {
genericPoster() {
return !this.movie?.poster_path
? this.genericPosterBig
: `https://image.tmdb.org/t/p/w500/${this.movie?.poster_path}`;
},
}
});
</script>
Когда есть постер фильма, перед его загрузкой на долю секунды появляется общий постер, но видимый.
Вы можете увидеть Stackblitz ЗДЕСЬ.



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


Что-то вроде следующего должно помочь, добавлены строки, отмеченные **********
<template>
<div class = "poster-container text-center text-sm-start my-3">
<!-- ************ -->
<img v-if = "loaded"
:src = "genericPoster"
:alt = "movie?.title"
class = "img-fluid shadow-sm"
/>
</div>
</template>
<script lang = "ts">
import { defineComponent } from "vue";
import { useRoute } from "vue-router";
import axios from "axios";
import env from "../env";
import { Movie } from "../models/Movie";
export default defineComponent({
name: "MovieDetails",
data() {
return {
//************************
loaded: false,
route: useRoute(),
genericPosterBig: require("../assets/generic-poster-big.png"),
movie: null as Movie | null
};
},
mounted() {
this.getMovieDetails();
},
methods: {
getMovieDetails() {
axios
.get(
`${env.api_url}/movie/${this.route.params.id}?api_key=${env.api_key}`
)
.then((response) => {
this.movie = response.data;
})
.catch((err) => console.info(err))
//****************
.finally(() => this.loaded = true);
},
computed: {
genericPoster() {
return !this.movie?.poster_path
? this.genericPosterBig
: `https://image.tmdb.org/t/p/w500/${this.movie?.poster_path}`;
},
}
});
</script>
Такое поведение ожидается с учетом вашего решения, но я понимаю, что вы хотите улучшить его по причинам UX.
Проблема в том, что ваш компонент монтируется до того, как данные станут доступны, поэтому вам нужно установить логику того, что делать во время его загрузки.
Я вижу два решения для этого:
Пока он загружается, используйте счетчик загрузки на вашем изображении во время его загрузки, а не заполнитель изображения (который должен появиться только после того, как будут получены детали постера). Это полезно, чтобы избежать мерцания макета: он резервирует пространство, необходимое для изображения.
<template>
<div class = "poster-container text-center text-sm-start my-3">
<div v-if = "loading">Loading spinner...</div>
<img
v-else
:src = "movie.poster_path"
:alt = "movie.title"
class = "img-fluid shadow-sm"
@error = "setPlaceholder"
/>
</div>
</template>
<script>
{
data() {
return {
movie: null,
loading: true,
genericPosterBig: null,
}
}
mounted() {
this.getMovieDetails()
},
methods: {
getMovieDetails() {
this.loading = true
axios.get(`${env.api_url}/movie/${this.route.params.id}?api_key=${env.api_key}`)
.then(({ data }) => {
this.movie = data;
})
.catch((err) => console.info(err))
.finally(() => {
this.loading = false
});
},
setPlaceholder(event) {
// This loads the image only if it's not already assigned
this.genericPosterBig ||= require("../assets/generic-poster-big.png")
event.target.src = this.genericPosterBig
}
}
}
</script>
{
// This will block the page rendering until the promise is settled.
async asyncData() {
const movieDetails = await axios
.get(`${env.api_url}/movie/${this.route.params.id}?api_key=${env.api_key}`)
.then((response) => response.data)
return { movieDetails }
}
}
поскольку значение загрузки «по умолчанию» равно false, не приведет ли это к «вспышке» пустого тега <img>? так как вы также удалили определение genericPoster, так что же будет делать :src = "genericPoster"?
Я имею в виду, что это просто пример реализации, а не копируемое решение. Я обновил его, чтобы код стал более правильным, но вы действительно должны изменить несколько вещей.
Вы должны загружать общий постер только в том случае, если изображение не загружается или если у вас нет URL-адреса для этого фильма, и поместить загрузчик до того, как URL-адрес изображения станет доступен. Поместите
v-if = "movie"на свой тег img и поместите загрузчик вместоv-else. Затем загрузите свое изображение или общий постер, если у вас нет URL-адреса изображения, но есть фильм. Вы можете добавить прослушиватель@errorповерх этого, чтобы поместить общий постер в img в случае ошибки с вашим постером фильма.