You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Vue+Vuex+Vue-router图片作品集的状态结构与数据获取咨询

嘿,这个问题在Vue+Vuex开发分页类项目里挺常见的,我来分享下实际项目里验证过的可行方案,帮你理清思路~

核心方案选型:不推荐单一images: []存所有图片

先直接说结论:如果你的图片量较大(超过几十张),单一数组存所有图片再用getter过滤的方案并不合适。原因很简单:

  • 分页接口是分批返回数据的,你不可能一次性拉取所有页的数据(带宽和内存消耗都很大);
  • 不同页面/标签的分页状态(当前页码、是否还有更多数据)没法独立维护,容易出现逻辑混乱。

更优的方案是按页面/标签拆分状态,同时缓存已加载数据,下面是具体的设计思路:

一、Vuex状态结构设计

我们可以把状态拆分为「首页专属」「标签页集合」「全局图片缓存」三部分,结构如下:

state: {
  // 首页分页相关状态
  homepage: {
    images: [],       // 已加载的首页图片
    currentPage: 1,   // 当前页码
    hasMore: true,    // 是否还有更多数据可加载
    isLoading: false  // 加载状态(防止重复请求)
  },
  // 标签页集合:用slug作为key,每个key对应独立的分页状态
  tagCollections: {},
  // 可选:全局缓存所有已加载的图片(跨页面复用,避免重复存储)
  cachedImages: {}
}

每个部分的作用:

  1. homepage:专门维护首页的分页逻辑,和其他页面完全隔离;
  2. tagCollections:每个标签页的分页状态独立存储,切换路由时已加载的数据不会丢失,再次进入无需重新请求;
  3. cachedImages:如果同一张图片可能出现在多个页面(比如某张婚礼照既在首页又在weddings标签页),按图片id缓存可以节省内存,也能提升组件渲染效率。

二、数据获取的Actions与Mutations实现

1. 首页加载逻辑

actions: {
  async loadHomepageImages({ commit, state }) {
    // 防止重复请求或无更多数据时触发
    if (state.homepage.isLoading || !state.homepage.hasMore) return;
    
    commit('SET_HOMEPAGE_LOADING', true);
    try {
      const res = await fetch(`../images/homepage?p=${state.homepage.currentPage}`);
      const newImages = await res.json();
      
      if (newImages.length === 0) {
        // 返回空数组说明没有更多数据了
        commit('SET_HOMEPAGE_HAS_MORE', false);
      } else {
        // 追加新图片,同时更新全局缓存
        commit('ADD_HOMEPAGE_IMAGES', newImages);
        commit('UPDATE_CACHED_IMAGES', newImages);
        commit('INCREMENT_HOMEPAGE_PAGE');
      }
    } catch (err) {
      console.error('加载首页图片失败:', err);
    } finally {
      commit('SET_HOMEPAGE_LOADING', false);
    }
  }
},
mutations: {
  SET_HOMEPAGE_LOADING(state, loading) {
    state.homepage.isLoading = loading;
  },
  ADD_HOMEPAGE_IMAGES(state, images) {
    state.homepage.images = [...state.homepage.images, ...images];
  },
  SET_HOMEPAGE_HAS_MORE(state, hasMore) {
    state.homepage.hasMore = hasMore;
  },
  INCREMENT_HOMEPAGE_PAGE(state) {
    state.homepage.currentPage += 1;
  },
  UPDATE_CACHED_IMAGES(state, images) {
    images.forEach(img => {
      state.cachedImages[img.id] = img;
    });
  }
}

2. 标签页加载逻辑

和首页逻辑类似,但需要针对不同slug初始化独立状态:

actions: {
  async loadTagImages({ commit, state }, tagSlug) {
    // 首次进入标签页时初始化状态
    if (!state.tagCollections[tagSlug]) {
      commit('INIT_TAG_COLLECTION', tagSlug);
    }
    const tagState = state.tagCollections[tagSlug];
    
    if (tagState.isLoading || !tagState.hasMore) return;
    commit('SET_TAG_LOADING', { tagSlug, loading: true });
    
    try {
      const res = await fetch(`../images/${tagSlug}?p=${tagState.currentPage}`);
      const newImages = await res.json();
      
      if (newImages.length === 0) {
        commit('SET_TAG_HAS_MORE', { tagSlug, hasMore: false });
      } else {
        commit('ADD_TAG_IMAGES', { tagSlug, images: newImages });
        commit('UPDATE_CACHED_IMAGES', newImages);
        commit('INCREMENT_TAG_PAGE', tagSlug);
      }
    } catch (err) {
      console.error(`加载标签${tagSlug}图片失败:`, err);
    } finally {
      commit('SET_TAG_LOADING', { tagSlug, loading: false });
    }
  }
},
mutations: {
  INIT_TAG_COLLECTION(state, tagSlug) {
    state.tagCollections[tagSlug] = {
      images: [],
      currentPage: 1,
      hasMore: true,
      isLoading: false
    };
  },
  SET_TAG_LOADING(state, { tagSlug, loading }) {
    state.tagCollections[tagSlug].isLoading = loading;
  },
  ADD_TAG_IMAGES(state, { tagSlug, images }) {
    state.tagCollections[tagSlug].images = [...state.tagCollections[tagSlug].images, ...images];
  },
  SET_TAG_HAS_MORE(state, { tagSlug, hasMore }) {
    state.tagCollections[tagSlug].hasMore = hasMore;
  },
  INCREMENT_TAG_PAGE(state, tagSlug) {
    state.tagCollections[tagSlug].currentPage += 1;
  }
}

三、组件中实现无限滚动

1. 首页组件示例

<template>
  <div class="homepage">
    <img 
      v-for="img in homepageImages" 
      :key="img.id" 
      :src="img.path" 
      :alt="img.title" 
      class="portfolio-img"
    />
    <div v-if="hasMore" class="loading-text" v-show="isLoading">加载中...</div>
  </div>
</template>

<script>
import { mapState, mapActions } from 'vuex';

export default {
  name: 'Homepage',
  mounted() {
    // 首次加载第一页
    this.loadHomepageImages();
    // 监听滚动实现无限加载
    window.addEventListener('scroll', this.handleScroll);
  },
  beforeDestroy() {
    window.removeEventListener('scroll', this.handleScroll);
  },
  methods: {
    ...mapActions(['loadHomepageImages']),
    handleScroll() {
      // 判断是否滚动到页面底部附近(100px阈值)
      const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
      const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight;
      const clientHeight = document.documentElement.clientHeight || window.innerHeight;
      
      if (scrollTop + clientHeight >= scrollHeight - 100 && !this.isLoading && this.hasMore) {
        this.loadHomepageImages();
      }
    }
  },
  computed: {
    ...mapState({
      homepageImages: state => state.homepage.images,
      isLoading: state => state.homepage.isLoading,
      hasMore: state => state.homepage.hasMore
    })
  }
};
</script>

2. 标签页组件示例(路由portfolio/:tagSlug)

<template>
  <div class="tag-page">
    <img 
      v-for="img in tagImages" 
      :key="img.id" 
      :src="img.path" 
      :alt="img.title" 
      class="portfolio-img"
    />
    <div v-if="hasMore" class="loading-text" v-show="isLoading">加载中...</div>
  </div>
</template>

<script>
import { mapState, mapActions } from 'vuex';

export default {
  name: 'TagPage',
  mounted() {
    this.loadTagImages(this.$route.params.tagSlug);
    window.addEventListener('scroll', this.handleScroll);
  },
  beforeDestroy() {
    window.removeEventListener('scroll', this.handleScroll);
  },
  watch: {
    // 切换标签时自动加载对应数据
    '$route.params.tagSlug'(newSlug) {
      this.loadTagImages(newSlug);
    }
  },
  methods: {
    ...mapActions(['loadTagImages']),
    handleScroll() {
      const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
      const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight;
      const clientHeight = document.documentElement.clientHeight || window.innerHeight;
      
      if (scrollTop + clientHeight >= scrollHeight - 100 && !this.isLoading && this.hasMore) {
        this.loadTagImages(this.$route.params.tagSlug);
      }
    }
  },
  computed: {
    ...mapState({
      tagImages: state => {
        const slug = this.$route.params.tagSlug;
        return state.tagCollections[slug] ? state.tagCollections[slug].images : [];
      },
      isLoading: state => {
        const slug = this.$route.params.tagSlug;
        return state.tagCollections[slug] ? state.tagCollections[slug].isLoading : false;
      },
      hasMore: state => {
        const slug = this.$route.params.tagSlug;
        return state.tagCollections[slug] ? state.tagCollections[slug].hasMore : true;
      }
    })
  }
};
</script>

四、补充优化建议

  1. 路由预加载:可以在路由守卫(beforeEnter)中提前加载标签页的第一页数据,提升用户体验;
  2. 防抖处理:给滚动监听添加防抖,避免频繁触发请求(比如用lodash.debounce);
  3. 状态清理:如果需要在离开标签页时清空对应状态,可以在beforeRouteLeave钩子中调用mutation清理;
  4. 接口过滤优先:尽量让后端接口返回符合条件的图片(比如首页接口只返回show_home: true的),减少前端过滤的计算量。

内容的提问来源于stack exchange,提问作者Glidarn

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.06 20:49:05