跳到正文
Joeplover
后端开发·2026-06-24·约 4 分钟阅读

短视频平台前端:Vue 3 + Tailwind CSS

基于 Vue 3 + Vite + Tailwind CSS 的短视频平台前端项目,响应式移动端设计。

手机短视频

短视频平台前端:Vue 3 + Tailwind CSS 实现类抖音体验

项目背景

这个项目是一个 Web 端的短视频浏览平台,目标是在浏览器中复刻抖音的滑动浏览体验。和大多数短视频 App 不同,我们选择 Web 端而非原生 App,核心考量是跨平台即时可用——用户打开浏览器就能刷视频,无需安装。技术栈选择了 Vue 3(Composition API)+ Tailwind CSS + Vite。

核心交互:滑动切换

短视频最核心的交互是上下滑动切换视频。我们放弃了传统的 scroll 容器方案,改用 touch 事件 + CSS transform 动画 来实现:

<script setup>
const currentIndex = ref(0)
const startY = ref(0)
const isDragging = ref(false)
const translateY = ref(0)

const onTouchStart = (e) => {
  startY.value = e.touches[0].clientY
  isDragging.value = true
}

const onTouchMove = (e) => {
  if (!isDragging.value) return
  const diff = e.touches[0].clientY - startY.value
  translateY.value = diff
}

const onTouchEnd = () => {
  isDragging.value = false
  // 滑动超过屏幕 20% 就切换
  if (Math.abs(translateY.value) > window.innerHeight * 0.2) {
    if (translateY.value < 0 && currentIndex.value < videos.length - 1) {
      currentIndex.value++
    } else if (translateY.value > 0 && currentIndex.value > 0) {
      currentIndex.value--
    }
  }
  translateY.value = 0
}
</script>

每个视频卡片占据 100vh,通过 transform: translateY() 实现滑动跟随,松手后判断是否超过阈值(屏幕高度的 20%)。超过则切换到下一视频,不到则回弹。

为什么不直接使用原生 scroll?因为我们需要更精细的滑动控制(比如阻尼效果、边缘吸附、手势方向锁定),原生 scroll 在这些方面自定义空间有限。

自动播放管理

使用 IntersectionObserver 管理视频播放:

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    const video = entry.target as HTMLVideoElement
    if (entry.isIntersecting) {
      video.play().catch(() => {})
    } else {
      video.pause()
      video.currentTime = 0
    }
  })
}, { threshold: 0.5 })

关键设计决策:只有当视频进入视口超过 50% 时才播放,离开就暂停并回到开头。这样保证用户切走时不会浪费带宽和内存。

虚拟列表与性能优化

如果 DOM 中同时存在几十个视频元素,浏览器内存会暴涨。我们采用 虚拟列表 + 仅渲染可见卡片 的策略:

  • 只渲染 currentIndex - 1 到 currentIndex + 1 的 3 个卡片
  • 上方和下方的卡片用占位 DIV 占据空间
  • 视频 DOM 元素的创建和销毁通过 v-if 控制
<template>
  <div class="snap-container" ref="containerRef">
    <div :style="{ height: currentIndex * 100 + 'vh' }" />
    <div v-for="i in visibleRange" :key="videos[i].id" class="video-card">
      <video v-if="Math.abs(i - currentIndex) <= 1" :src="videos[i].url" />
    </div>
    <div :style="{ height: (videos.length - 1 - currentIndex) * 100 + 'vh' }" />
  </div>
</template>

流量优化

移动端流量敏感,做了三个优化:

  1. 网络类型检测:通过 navigator.connection.effectiveType 判断网络(4G/WiFi/2G),4G 以上才预加载下一视频的封面图
  2. 预加载控制:只预加载下一个视频的元数据(preload="metadata"),不是整个视频文件
  3. 内存回收:离开视频页面时主动销毁所有视频元素、释放 URL.createObjectURL 创建的对象

踩坑记录

1. iOS Safari 视频自动播放限制

iOS 上 <video> 标签的 autoplay 必须同时满足 muted + playsinline 才能生效。没加 playsinline 时视频会全屏播放,体验很糟糕。

2. Touch 事件与滚动冲突

页面存在纵向滚动区域时,touchmove 事件的 preventDefault() 需要谨慎使用,否则页面其他部分无法滚动。解决方案:只在视频容器区域内阻止默认行为。

3. 内存泄漏

初期发现刷 30-50 个视频后页面卡顿。排查发现是 URL.createObjectURL 创建的 Blob URL 没有在视频销毁时 revoke。添加组件 onUnmounted 钩子统一清理。

同类方案对比

如果追求更低成本的实现,可以考虑使用 @vueuse/core 的 useSwipe composable,它封装了 touch 事件的细节。但我们这套手动实现更适合需要精细自定义的场景。