导读: 最近在做前端工作项目中做了一个功能,要求在界面中可以预览显示图片,且支持常规的左右滑动,以及切题,同时在 H5 页面也要支持图片的缩放。
由于之前使用的 Swiper 都是属于低版本,按照官网的使用案例,并无不妥之处;但是当在 Vue3 环境下使用后,尤其是当 Swiper 版本更新到 v11.1.1 之后,会出现一些使用上的改变,故此动动手指,敲敲键盘,记录一下~
效果图

背景
其实如果只是想实现一个图片预览功能,完全可以手撸一个,亦或者找一些现成的三方库,比如 Element Plus 的 Image Viewer API、以及 Vant UI 的 ImagePreview 组件。
不过由于定制化的效果和我们本期项目需求有些差异,就想着使用 Swiper 了。
Swiper 旧版使用
其中 Swiper 在 Vue2.x 中的写法,亦或者是 Vue3.x 但不是 setup 语法糖样式的写法,都是和 Swiper 官网上的提供的写法大同小异,而且核心的配置也都是通过 options 进行配置的,亦或者是通过创建的时候进行配置。
// Vue 2.x ---------
<template>
  <swiper
    ref="mySwiper"
    class="swiper-wrapper"
    :options="swiperOption"
    @slide-change="slideChange"
  >
    <swiper-slide
      v-for="(item, index) in imgUrls"
      :key="index"
      class="swiper-item swiper-zoom-container"
    >
      <img :src="item" alt="" />
    </swiper-slide>
  </swiper>
</template>
<script>
import { swiper, swiperSlide } from "vue-awesome-swiper";
export default {
  name: "SwiperTest",
  components: {
    swiper,
    swiperSlide,
  },
  props: {
    imgUrls: {
      type: Array,
      default: () => [],
    },
  },
  data() {
    return {
      swiperOption: {
        pagination: {
          el: ".swiper-pagination",
          type: "fraction",
        },
        navigation: {
          nextEl: ".icon-jiantouyou",
          prevEl: ".icon-jiantouzuo",
          disabledClass: "swiper-paperChange-disabled",
        },
        zoom: true,
      },
      curIndex: 0,
    };
  },
  computed: {
    swiper() {
      return this.$refs.mySwiper && this.$refs.mySwiper.swiper;
    },
    slideIndex() {
      return `${this.curIndex + 1} / ${this.imgUrls.length}`;
    },
  },
  methods: {
    sizeChange() {
      setTimeout(() => {
        this.swiper.update();
      }, 0);
    },
    slideChange() {
      this.curIndex = this.swiper.activeIndex;
    },
  },
};
</script>
Swiper 在 Vue3 的 setup 语法糖中的写法
如果是在 TypeScript 语法环境下 Vue3.x 的 setup 语法糖中的写法,如果通过 new Swiper() 创建的时候配置属性,会报 TypeScript class constructor without ‘new’ cannot be invoked 类似的错误,主要还是因为写法的变化引起的。
说明:
- 说明一下,此处示例中的
分页指示器,并没有使用官方的Pagination Module模块;- 主要是由于项目需求和官方指示器功能略有不同,而且项目中指示器显示的数字是根据服务端返回的,并非固定顺序,完全有可能是乱序的。
默认情况下,Swiper Vue 使用 Swiper 的核心版本(没有任何附加模块),如果要使用 Navigation、Pagination、Zoom 等模块,必须先安装它们。
以下列举 Swiper 相关扩展模块:
| 模块名称 | 模块描述 | 
|---|---|
| Virtual | Virtual Slides module,用于创建虚拟幻灯片。 | 
| Keyboard | Keyboard Control module,通过键盘控制幻灯片的切换。 | 
| Mousewheel | Mousewheel Control module,通过鼠标滚轮控制幻灯片的切换。 | 
| Navigation | Navigation module,提供导航按钮以控制幻灯片的切换。 | 
| Pagination | Pagination module,提供分页器以控制幻灯片的切换。 | 
| Scrollbar | Scrollbar module,提供滚动条以控制幻灯片的滚动。 | 
| Parallax | Parallax module,创建具有透视效果的幻灯片背景。 | 
| FreeMode | Free Mode module,允许自由拖动和缩放幻灯片。 | 
| Grid | Grid module,将幻灯片组织成网格布局。 | 
| Manipulation | Slides manipulation module,提供对幻灯片的各种操作和调整功能。 | 
| Zoom | Zoom module,用于实现缩放功能 | 
| Controller | Controller module,用于控制视频播放、暂停等操作 | 
| A11y | Accessibility module,提高应用程序的可访问性,使残障人士也能方便地使用应用程序 | 
| History | History Navigation module,用于实现历史记录的导航功能 | 
| HashNavigation | Hash Navigation module,通过 URL 的哈希部分实现页面的导航功能 | 
| Autoplay | Autoplay module,用于实现自动播放功能 | 
| EffectFade | Fade Effect module,实现淡入淡出效果 | 
| EffectCube | Cube Effect module,实现立方体旋转效果 | 
| EffectFlip | Flip Effect module,实现翻页效果 | 
| EffectCoverflow | Cover Effect module,实现覆盖层效果 | 
| EffectCards | Cards Effect module | 
| EffectCreative | Creative Effect module | 
| Thumbs | Thumbs module | 
// Vue3.x 的 setup 语法糖中的写法 ---------
<script setup lang="ts">
import { useRoute } from "vue-router";
import { useShareStore } from "@share/stores/modules/share";
import { Ref, computed, onMounted, ref, watch } from "vue";
import { Swiper, SwiperSlide } from "swiper/vue";
import type { Swiper as SwiperObj } from "swiper/types";
import { Zoom } from "swiper/modules";
import "swiper/css";
import "swiper/css/zoom";
import { isWeixinBrowser } from "@share/utils/device";
import switchPrevGrayIcon from "@share/assets/icons/icon_arrow_left_gray.png";
import switchPrevLightGrayIcon from "@share/assets/icons/icon_arrow_left_light_gray.png";
import switchNextGrayIcon from "@share/assets/icons/icon_arrow_right_gray.png";
import switchNextLightGrayIcon from "@share/assets/icons/icon_arrow_right_light_gray.png";
const route = useRoute();
const shareStore = useShareStore();
const isLoading = ref(false);
// eslint-disable-next-line no-undef
const imgList: Ref<ShareTopicQuestionInfo[]> = ref([]);
const pageNum = 5;
const curPage = ref(0);
const curIndex = ref(0);
const swiperRef = ref<SwiperObj>();
const isWeixin = ref(false);
const curPageList = computed(() => {
  const list: number[] = [];
  let count = 0;
  while (
    curPage.value * pageNum + count < imgList.value.length &&
    count < pageNum
  ) {
    count++;
    list.push(curPage.value * pageNum + count);
  }
  return list;
});
const isLastPage = computed(() => {
  return (curPage.value + 1) * pageNum >= imgList.value.length;
});
onMounted(() => {
  isWeixin.value = isWeixinBrowser();
  isLoading.value = true;
  shareStore.getShareDetail(route.query.shareId as string);
});
watch(
  () => shareStore.shareTopic,
  () => {
    document.title = shareStore.shareTopic.title;
    imgList.value = shareStore.shareTopic.questionInfos;
    isLoading.value = false;
    if (swiperRef.value && swiperRef.value.zoom) {
      swiperRef.value.zoom.enabled = true;
    }
  }
);
const getSwitchIcon = (isNext: boolean) => {
  if (isNext) {
    return isLastPage.value ? switchNextLightGrayIcon : switchNextGrayIcon;
  } else {
    return curPage.value === 0 ? switchPrevLightGrayIcon : switchPrevGrayIcon;
  }
};
const onSwiper = (swiper: SwiperObj) => {
  swiperRef.value = swiper;
};
const onSlideChangeEvent = (swiper: SwiperObj) => {
  curIndex.value = swiper.activeIndex;
  curPage.value = Math.floor(curIndex.value / pageNum);
  // 修正由于 swiper-wrapper 的 transform 属性引起的滚动偏移问题
  const swiperEl = document.querySelector(".swiper-wrapper") as HTMLElement;
  if (swiperEl) {
    const rect = swiperEl.getBoundingClientRect();
    swiperEl.style.transform = `translate3d(-${
      rect.width * curIndex.value
    }px, 0px, 0px)`;
  }
};
const clickTopicEvent = (index: number) => {
  curIndex.value = index - 1;
  curPage.value = Math.floor(curIndex.value / pageNum);
  swiperRef.value?.slideTo(curIndex.value);
};
const clickPrevEvent = () => {
  if (curPage.value === 0) return;
  curPage.value--;
};
const clickNextEvent = () => {
  if (isLastPage.value) return;
  curPage.value++;
};
const clickCloseEvent = () => {
  window.close();
};
</script>
<template>
  <div class="share-topic-wrapper">
    <template v-if="isLoading">
      <div
        class="loading-wrapper"
        v-loading="isLoading"
        element-loading-text="精彩内容马上呈现"
        element-loading-background="rgba(1, 1, 1, 0.01)"
      >
        <img
          class="loading-background"
          src="@share/assets/images/image_placeholder_loading.png"
          alt=""
        />
        <img
          class="loading-logo-zhc"
          src="@share/assets/images/image_logo_zhc.png"
          alt=""
        />
        <img
          class="loading-logo-iflyspark"
          src="@share/assets/images/image_logo_iflyspark.png"
          alt=""
        />
      </div>
    </template>
    <template v-else>
      <div v-if="!isWeixin" class="navigation-wrapper">
        <div class="navigation-close" @click.stop="clickCloseEvent">
          <img src="@share/assets/icons/icon_close_black.png" alt="" />
        </div>
        <span class="navigation-title"></span>
        <div class="navigation-right"></div>
      </div>
      <div class="pagination-wrapper">
        <div
          v-show="imgList.length > pageNum"
          class="prev-button button switch-button"
          @click.stop="clickPrevEvent"
        >
          <img :src="getSwitchIcon(false)" alt="" />
        </div>
        <div
          class="list-button-wrapper"
          :class="{ notFull: curPageList.length < pageNum }"
        >
          <template v-for="item in curPageList" :key="item">
            <div
              class="item-button button"
              :class="{ active: curIndex + 1 === item }"
              @click.stop="clickTopicEvent(item)"
            >
              
            </div>
          </template>
        </div>
        <div
          v-show="imgList.length > pageNum"
          class="next-button button switch-button"
          @click.stop="clickNextEvent"
        >
          <img :src="getSwitchIcon(true)" alt="" />
        </div>
      </div>
      <div class="content-wrapper">
        <swiper
          class="img-list swiper-container"
          :modules="[Zoom]"
          :zoom="true"
          :maxRatio="5"
          @swiper="onSwiper"
          @slideChange="onSlideChangeEvent"
        >
          <swiper-slide
            v-for="(item, index) in imgList"
            :key="index"
            class="swiper-item"
          >
            <div class="swiper-zoom-container">
              <img class="img-item" :src="item.resourceUrl" />
            </div>
          </swiper-slide>
        </swiper>
      </div>
    </template>
  </div>
</template>
<style scoped lang="less">
.share-topic-wrapper {
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100%;
  font-size: 16px;
}
.loading-wrapper {
  width: 100%;
  height: 100%;
  .loading-background {
    width: 100%;
    height: 100%;
  }
  .loading-logo-zhc {
    position: fixed;
    top: 110px;
    left: 40px;
    width: 40%;
  }
  .loading-logo-iflyspark {
    position: fixed;
    right: 40px;
    bottom: 56px;
    width: 30%;
  }
  :deep(.el-loading-spinner .path) {
    stroke: var(--color-blue) !important;
    stroke-width: 5px;
  }
  :deep(.el-loading-text) {
    font-size: 24px;
    color: var(--text-gray) !important;
  }
}
.navigation-wrapper {
  display: flex;
  align-items: center;
  width: 100%;
  height: 80px;
  .navigation-close {
    width: 40px;
    height: 40px;
    margin-left: 30px;
    img {
      width: 100%;
      height: 100%;
    }
  }
  .navigation-title {
    flex: 1;
    height: 43px;
    margin: 0 10px;
    font-size: 36px;
    font-weight: bold;
    line-height: 43px;
    text-align: center;
  }
  .navigation-right {
    width: 40px;
    height: 40px;
    margin-right: 30px;
  }
}
.pagination-wrapper {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 80px;
  margin: 32px 10px;
  .list-button-wrapper {
    display: flex;
    flex: 1;
    align-items: center;
    justify-content: space-between;
    height: 100%;
    padding: 0 1px;
    margin: 0 20px;
    &.notFull {
      justify-content: center;
      .item-button {
        margin: 0 16px;
      }
    }
  }
  .button {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 80px;
    min-width: 80px;
    height: 80px;
    min-height: 80px;
    font-size: 40px;
    font-weight: 600;
    cursor: pointer;
    border: 1px solid var(--border-light-gray);
    border-radius: 12px;
    &.switch-button {
      border: 1px solid transparent;
    }
    &.active {
      color: var(--color-white);
      background-color: var(--bg-blue);
    }
    img {
      width: 60px;
      height: 60px;
    }
  }
}
.content-wrapper {
  flex: 1;
  margin: 0 30px;
  .img-list {
    width: 100%;
    height: 100%;
    .swiper-item {
      max-width: 100%;
      max-height: 100%;
      border: 1px solid transparent;
      .swiper-zoom-container {
        display: flex;
        align-items: center;
        max-width: 100%;
        max-height: 100%;
        .img-item {
          max-width: 100%;
          height: auto;
        }
      }
    }
  }
}
</style>
2024.04.26 更新
说一下最近由于
Swiper遇到的坑位点
Swiper 滚动偏移导致的两侧出现前一页面和后一页面内容

该现象的实际效果是:
- 最开始的几页滚动是没有异常的;
- 但是当页数越来越多的时候,当前页两侧会出现垂直状黑色细条;
- 经排查,发现该细条是属于前一页或后一页的图片。

原因排查:
- 由于排查发现该黑色细条属于前一页或后一页的图片内容,则得出结论是属于内容发生了偏移;
- 而正常情况下,Swiper滚动是不会发生偏移的,然后就查询了一下当前滚动对象swiper-wrapper类的transform属性;
- 经观察发现,每个单位的 div.swiper-slide的渲染宽度为358.81px,而每次swiper-wrapper滚动是transform变化单位是359px,存在了一定的偏差;
- 所以当滚动的页数达到一定量级的时候,这个偏差值就会变得很大,也就出现了上面的内容偏移。
问题解决:
- 既然是由于 swiper-wrapper的transform滚动没有按照实际值进行滚动,那就在滚动的时候对该值进行调整;
- 不能直接使用 clientWidth取值,该值也是transform实际滚动单位;
- 通过 getBoundingClientRect()方法获取实际当前swiper-wrapper渲染的尺寸,然后进行调整;
- 同时为了防止出现其它异常场景,可以对每个 swiper-item添加border: 1px solid transparent;透明边框属性,防止内容外溢。
<script setup lang="ts">
const onSlideChangeEvent = (swiper: SwiperObj) => {
  curIndex.value = swiper.activeIndex;
  curPage.value = Math.floor(curIndex.value / pageNum);
  // 修正由于 swiper-wrapper 的 transform 属性不允许有小数引起的滚动偏移问题
  const swiperEl = document.querySelector(".swiper-wrapper") as HTMLElement;
  if (swiperEl) {
    const rect = swiperEl.getBoundingClientRect();
    swiperEl.style.transform = `translate3d(-${
      rect.width * curIndex.value
    }px, 0px, 0px)`;
  }
};
</script>
参考链接
版权声明
原文作者:苜蓿鬼仙(苜蓿、jijiucheng)
原文链接:GitHub.io - 苜蓿鬼仙 - 【前端】Vue3 中 Swiper 的使用示例
发表日期:2024/04/23 16:30:00
更新日期:2024/04/26 10:00:00
-
GitHub:GitHub - jijiucheng
个人博客:GitHub.io - 苜蓿鬼仙
小专栏:小专栏 - 苜蓿鬼仙
掘金:掘金 - 苜蓿鬼仙
微博:微博 - 苜蓿鬼仙
公众号:微信 - 苜蓿小站
小程序:微信 - 苜蓿小站