【前端】Vue3 中 Swiper 的使用示例

2024/04/23 前端 共 10566 字,约 31 分钟

导读: 最近在做前端工作项目中做了一个功能,要求在界面中可以预览显示图片,且支持常规的左右滑动,以及切题,同时在 H5 页面也要支持图片的缩放。

由于之前使用的 Swiper 都是属于低版本,按照官网的使用案例,并无不妥之处;但是当在 Vue3 环境下使用后,尤其是当 Swiper 版本更新到 v11.1.1 之后,会出现一些使用上的改变,故此动动手指,敲敲键盘,记录一下~

效果图

背景

其实如果只是想实现一个图片预览功能,完全可以手撸一个,亦或者找一些现成的三方库,比如 Element PlusImage Viewer API、以及 Vant UIImagePreview 组件。

不过由于定制化的效果和我们本期项目需求有些差异,就想着使用 Swiper 了。

Swiper 旧版使用

其中 SwiperVue2.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.xsetup 语法糖中的写法,如果通过 new Swiper() 创建的时候配置属性,会报 TypeScript class constructor without ‘new’ cannot be invoked 类似的错误,主要还是因为写法的变化引起的。

说明:

  • 说明一下,此处示例中的 分页指示器,并没有使用官方的 Pagination Module 模块;
  • 主要是由于项目需求和官方指示器功能略有不同,而且项目中指示器显示的数字是根据服务端返回的,并非固定顺序,完全有可能是乱序的。

默认情况下,Swiper Vue 使用 Swiper 的核心版本(没有任何附加模块),如果要使用 Navigation、Pagination、Zoom 等模块,必须先安装它们。

以下列举 Swiper 相关扩展模块

模块名称模块描述
VirtualVirtual Slides module,用于创建虚拟幻灯片。
KeyboardKeyboard Control module,通过键盘控制幻灯片的切换。
MousewheelMousewheel Control module,通过鼠标滚轮控制幻灯片的切换。
NavigationNavigation module,提供导航按钮以控制幻灯片的切换。
PaginationPagination module,提供分页器以控制幻灯片的切换。
ScrollbarScrollbar module,提供滚动条以控制幻灯片的滚动。
ParallaxParallax module,创建具有透视效果的幻灯片背景。
FreeModeFree Mode module,允许自由拖动和缩放幻灯片。
GridGrid module,将幻灯片组织成网格布局。
ManipulationSlides manipulation module,提供对幻灯片的各种操作和调整功能。
ZoomZoom module,用于实现缩放功能
ControllerController module,用于控制视频播放、暂停等操作
A11yAccessibility module,提高应用程序的可访问性,使残障人士也能方便地使用应用程序
HistoryHistory Navigation module,用于实现历史记录的导航功能
HashNavigationHash Navigation module,通过 URL 的哈希部分实现页面的导航功能
AutoplayAutoplay module,用于实现自动播放功能
EffectFadeFade Effect module,实现淡入淡出效果
EffectCubeCube Effect module,实现立方体旋转效果
EffectFlipFlip Effect module,实现翻页效果
EffectCoverflowCover Effect module,实现覆盖层效果
EffectCardsCards Effect module
EffectCreativeCreative Effect module
ThumbsThumbs 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-wrappertransform 滚动没有按照实际值进行滚动,那就在滚动的时候对该值进行调整;
  • 不能直接使用 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

-

GitHubGitHub - jijiucheng

个人博客GitHub.io - 苜蓿鬼仙

小专栏小专栏 - 苜蓿鬼仙

掘金掘金 - 苜蓿鬼仙

微博微博 - 苜蓿鬼仙

公众号微信 - 苜蓿小站

小程序微信 - 苜蓿小站

文档信息

Search

    Table of Contents