如何在这个Vue 3应用中防止临时加载占位图像?

huangapple go评论65阅读模式
英文:

How can I prevent the temporary loading of a placeholder image in this Vue 3 app?

问题

I have been working on an SPA with Vue 3, TypeScript and The Movie Database (TMDB) API.

我一直在使用 Vue 3TypeScriptThe Movie Database (TMDB) API 开发单页面应用 (SPA)。

I want to display the movie poster on the movie details page if there is a poster, otherwise I want to display a placeholder (generic-poster-big.png).

如果有电影海报,我希望在电影详情页面上显示电影海报,否则我希望显示一个占位符 (generic-poster-big.png)。

For this purpose, in src\components\MovieDetails.vue, I have:

为了实现这个目的,在 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.log(err));
    },
  },

  computed: {
    genericPoster() {
      return !this.movie?.poster_path
        ? this.genericPosterBig
        : `https://image.tmdb.org/t/p/w500/${this.movie?.poster_path}`;
    },
  },
});
</script>

The problem

问题是:

When there is a movie poster, before it loads, the generic poster appears, for a fraction of a second, yet visible.

当有电影海报时,在电影海报加载之前,通常会短暂地显示占位符,尽管它是可见的。

Questions

问题是:

  1. What am I doing wrong?

    我做错了什么?

  2. What is the most reliable way to fix this problem?

    什么是最可靠的方法来解决这个问题?

英文:

I have been working on an SPA with Vue 3, TypeScript and The Movie Database (TMDB) API.

I want to display the movie poster on the movie details page if there is a poster, otherwise I want to display a placeholder (generic-poster-big.png).

For this purpose, in src\components\MovieDetails.vue, I have:

&lt;template&gt;
  &lt;div class=&quot;poster-container text-center text-sm-start my-3&quot;&gt;
	&lt;img
	  :src=&quot;genericPoster&quot;
	  :alt=&quot;movie?.title&quot;
	  class=&quot;img-fluid shadow-sm&quot;
	/&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script lang=&quot;ts&quot;&gt;
import { defineComponent } from &quot;vue&quot;;
import { useRoute } from &quot;vue-router&quot;;
import axios from &quot;axios&quot;;
import env from &quot;../env&quot;;
import { Movie } from &quot;../models/Movie&quot;;

export default defineComponent({
  name: &quot;MovieDetails&quot;,

  data() {
    return {
	  route: useRoute(),
	  genericPosterBig: require(&quot;../assets/generic-poster-big.png&quot;),
	  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) =&gt; {
          this.movie = response.data;
        })
        .catch((err) =&gt; console.log(err));
    },

  computed: {
    genericPoster() {
	  return !this.movie?.poster_path
		? this.genericPosterBig
		: `https://image.tmdb.org/t/p/w500/${this.movie?.poster_path}`;
	},
  }
});
&lt;/script&gt;

The problem

When there is a movie poster, before it loads, the generic poster appears, for a fraction of a second, yet visible.

Questions

  1. What am I doing wrong?
  2. What is the most reliable way to fix this problem?

答案1

得分: 4

以下是代码的翻译部分:

类似以下内容应该能解决问题,已标记的部分为新增内容

<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.log(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>

希望这个翻译对您有所帮助。如果您需要进一步的信息或翻译,请随时告诉我。

英文:

Something like the following should do the trick, added lines marked with **********

&lt;template&gt;
  &lt;div class=&quot;poster-container text-center text-sm-start my-3&quot;&gt;
    &lt;!-- ************ --&gt;
    &lt;img v-if=&quot;loaded&quot;
      :src=&quot;genericPoster&quot;
      :alt=&quot;movie?.title&quot;
      class=&quot;img-fluid shadow-sm&quot;
    /&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script lang=&quot;ts&quot;&gt;
import { defineComponent } from &quot;vue&quot;;
import { useRoute } from &quot;vue-router&quot;;
import axios from &quot;axios&quot;;
import env from &quot;../env&quot;;
import { Movie } from &quot;../models/Movie&quot;;

export default defineComponent({
  name: &quot;MovieDetails&quot;,

  data() {
    return {
      //************************
      loaded: false, 
      route: useRoute(),
      genericPosterBig: require(&quot;../assets/generic-poster-big.png&quot;),
      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) =&gt; {
          this.movie = response.data;
        })
        .catch((err) =&gt; console.log(err))
        //****************
        .finally(() =&gt; this.loaded = true); 
    },

  computed: {
    genericPoster() {
      return !this.movie?.poster_path
        ? this.genericPosterBig
        : `https://image.tmdb.org/t/p/w500/${this.movie?.poster_path}`;
    },
  }
});
&lt;/script&gt;

答案2

得分: 1

以下是翻译好的部分:

这种行为是因为你的解决方案而预期的,但我理解你希望为了用户体验而改进这一点。

问题在于在数据可用之前组件已挂载,所以你需要设置一个逻辑来处理加载过程中的情况。

我看到有两种解决方案:

  1. 仅在主要图像加载失败时(即URL错误或为空时)设置占位图像。因此,只有在尝试加载主要图像后才会加载占位图像,而不是在之前。

在加载过程中,使用图像的加载旋转器,而不是图像占位符(图像占位符应该在获取海报详情后才出现)。这有助于避免布局闪烁:它将保留所需的图像空间。

&lt;template&gt;
  &lt;div class=&quot;poster-container text-center text-sm-start my-3&quot;&gt;
    &lt;div v-if=&quot;loading&quot;&gt;加载中...&lt;/div&gt;
    &lt;img
      v-else
      :src=&quot;movie.poster_path&quot;
      :alt=&quot;movie.title&quot;
      class=&quot;img-fluid shadow-sm&quot;
      @error=&quot;setPlaceholder&quot;
    /&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
{
  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 }) =&gt; {
          this.movie = data;
        })
        .catch((err) =&gt; console.log(err))
        .finally(() =&gt; {
          this.loading = false
        });
    },
    setPlaceholder(event) {
       // 仅在尚未分配时加载图像
       this.genericPosterBig ||= require(&quot;../assets/generic-poster-big.png&quot;)
       event.target.src = this.genericPosterBig
    }
  }
}
&lt;/script&gt;
  1. 使用Nuxt框架,你可以在挂载电影详情页面之前预加载电影详情,使用useAsyncData()可组合(或选项API),这样你就可以确保在创建详情页面时已经拥有了所有电影数据(因此不会出现闪烁)。
{
  // 这将阻止页面渲染,直到承诺解决为止。
  async asyncData() {
     const movieDetails = await axios
        .get(`${env.api_url}/movie/${this.route.params.id}?api_key=${env.api_key}`)
        .then((response) =&gt; response.data)
     return { movieDetails }
  }
}

如果有任何其他翻译需求,请告诉我。

英文:

This behaviour is expected given your solution, but I understand you want to improve this for UX reasons.

The problem is that your component is mounted before the data is available, so you need to set a logic on what to do while it's loading

I see two solutions for this:

  1. Set your placeholder image only in case the main image fails to load (i.e. when the url is wrong or it's empty). So it will only loads your placeholder after trying to load the main image, not before.

While it loads, use a loading spinner on your image while it's loading, not the image placeholder (which should appear only once the poster details has been fetched). This is useful to avoid layout flickering: it will reserve the space required by the image.

&lt;template&gt;
  &lt;div class=&quot;poster-container text-center text-sm-start my-3&quot;&gt;
    &lt;div v-if=&quot;loading&quot;&gt;Loading spinner...&lt;/div&gt;
    &lt;img
      v-else
      :src=&quot;movie.poster_path&quot;
      :alt=&quot;movie.title&quot;
      class=&quot;img-fluid shadow-sm&quot;
      @error=&quot;setPlaceholder&quot;
    /&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
{
  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 }) =&gt; {
          this.movie = data;
        })
        .catch((err) =&gt; console.log(err))
        .finally(() =&gt; {
          this.loading = false
        });
    },
    setPlaceholder(event) {
       // This loads the image only if it&#39;s not already assigned
       this.genericPosterBig ||= require(&quot;../assets/generic-poster-big.png&quot;)
       event.target.src = this.genericPosterBig
    }
  }
}
&lt;/script&gt;
  1. Using the Nuxt framework, you could preload the movies details before mounting the movie details page using the useAsyncData() composable (or for option api), so you are sure you already have all the movies data when creating the details page (so no flickering).
{
  // 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) =&gt; response.data)
     return { movieDetails }
  }
}

答案3

得分: 1

以下是翻译好的部分:

显然,在尝试渲染海报之前,确保电影已加载,使用 v-if 解决了该问题:

<img v-if="movie"
     :src="genericPoster"
     :alt="movie?.title"
     class="img-fluid shadow-sm"
/>
英文:

Apparently, making sure that the movie is loaded before trying to render the poster at all, with v-if, fixed the issue:

 &lt;img v-if=&quot;movie&quot;
      :src=&quot;genericPoster&quot;
      :alt=&quot;movie?.title&quot;
      class=&quot;img-fluid shadow-sm&quot;
 /&gt;

huangapple
  • 本文由 发表于 2023年5月10日 14:55:35
  • 转载请务必保留本文链接:https://go.coder-hub.com/76215660.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定