## 项目介绍
> 前几天有给大家分享一个[vue3 web端聊天](https://studygolang.com/topics/13019)实例,今天分享的是最新开发的vue3仿抖音实战项目。
> [vue3Douyin仿抖音](https://juejin.cn/post/6924609446060490760/) 基于`vue3全家桶技术+有赞vant3组件库+vue3移动端弹框v3popup组件`搭建开发的一款`短视频/直播/聊天`实战案例。
![](https://oscimg.oschina.net/oscnet/up-ce5decefbdfeb41eff8437233809c3b83c5.png)
## 使用技术
- 编码+技术:vscode | vue3.0.5+vue-router+vuex4
- 组件库:vant3.x (有赞移动端vue3组件库)
- 弹层组件:v3popup(移动端vue3弹框组件)
- 字体图标:阿里iconfont图标
- 导航条+Tab栏:自定义顶部导航/底部标签栏组件
- 构建工具:vite.js
![](https://oscimg.oschina.net/oscnet/up-e47c619526f7efdfd1e53cc43e1133bc973.gif)
使用vant3来实现短视频/直播页的滑动切换功能。
![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8f0998578c734e6a91560864c5eeff87~tplv-k3u1fbpfcp-watermark.image)
## 项目结构
![](https://oscimg.oschina.net/oscnet/up-058a6cb943662a3868cb9d2c3384432cdf7.png)
整个项目采用vue3组合式语法来编码开发。
![](https://oscimg.oschina.net/oscnet/up-9d503ef011ffa37b3d6ece5423221fa6b52.png)
![](https://oscimg.oschina.net/oscnet/up-4165ee3510b7983a68d1af4d0e34b574010.png)
![](https://oscimg.oschina.net/oscnet/up-0c3ffeb68ee6e7a5e1d81adafdb5a975c8c.png)
![](https://oscimg.oschina.net/oscnet/up-9aa093e30531020bc735cb732a04476728d.png)
![](https://oscimg.oschina.net/oscnet/up-cbdb80e82667240ca9d80eb4f56ea6efa91.png)
![](https://oscimg.oschina.net/oscnet/up-a4056c0a54d5c744a33ebf82c7f8f154628.png)
![](https://oscimg.oschina.net/oscnet/up-1af70903652e285a1fc73c9daca5247a0ca.png)
![](https://oscimg.oschina.net/oscnet/up-ddd470047f9fa167db63db90c36e9c92118.png)
![](https://oscimg.oschina.net/oscnet/up-b5a10b75fbd3aa804500483de90946fc350.png)
## 自定义弹框v3popup
基于vue3.0开发的一款移动端对话框组件,支持多种弹窗类型及动画效果。
![](https://oscimg.oschina.net/oscnet/up-e153a4295576b63bcd477c27b6e61417a57.png)
在整个项目中都有应用到这个插件。支持函数式/标签式调用方式。
![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8c30ec5c0b574262b053ad2914d8d772~tplv-k3u1fbpfcp-watermark.image)
[vue3.x开发手机端自定义弹框组件](https://www.cnblogs.com/xiaoyan2017/p/14210820.html)
## 项目配置
vite.js构建功能创建的项目,会有一个vite.config.js配置文件。
```
/**
* vite.config.js项目配置
*/
import vue from '@vitejs/plugin-vue'
import path from 'path'
/**
* @type {import('vite').UserConfig}
*/
export default {
plugins: [vue()],
build: {
// 基本目录
// base: '/',
/**
* 输出文件目录
* <a href="/user/default" title="@default">@default</a> dist(默认)
*/
// outDir: 'target',
},
// 环境配置
server: {
// 自定义接口
port: 3000,
// 是否自动浏览器打开
open: false,
// 是否开启https
https: false,
// 代理配置
proxy: {
// ...
}
},
// 设置路径别名
alias: {
'@': path.resolve(__dirname, './src'),
'@components': path.resolve(__dirname, './src/components'),
'@views': path.resolve(__dirname, './src/views')
}
}
```
## vue3抽离公共组件
新建一个plugins.js来引入一些常用的公共组件。
```
/**
* vue3引入公共组件
*/
// 引入Vant3.x组件库
import Vant from 'vant'
import 'vant/lib/index.css'
// 引入Vue3.x移动端弹层组件
import V3Popup from '@components/v3popup'
import NavBar from '@components/navBar.vue'
import TabBar from '@components/tabBar.vue'
import Utils from './utils'
import Storage from './storage'
const Plugins = (app) => {
app.use(Vant)
app.use(V3Popup)
// 注册公用组件
app.component('navbar', NavBar)
app.component('tabbar', TabBar)
app.provide('utils', Utils)
app.provide('storage', Storage)
}
```
## vue3实现小视频/直播功能
大家可以看到小视频和直播模板有些地方的功能是通用的。都是使用swipe来实现滑动效果。
![](https://oscimg.oschina.net/oscnet/up-f1317e2ca89c1d7ab42b07739491f9a9b90.png)
整体分为顶部+视频区+底部三大部分。
```
<template>
<div class="bg-161823">
<!-- >>顶部NavBar -->
<navbar :back="false" bgcolor="transparent" transparent>
<template v-slot:title>
...
</template>
<template v-slot:right><div><i class="iconfont icon-search"></i></div></template>
</navbar>
<!-- >>主面板 -->
<div class="vui__scrollview flex1">
<div class="vui__swipeview">
<!-- ///滑动切换区 -->
<van-swipe ref="swipeHorizontalRef" :show-indicators="false" :loop="false" @change="handleSwipeHorizontal">
<van-swipe-item v-for="(item,index) in videoLs" :key="index">
<template v-if="item.category == 'nearby'">
<div class="swipe__nearLs">
...
</div>
</template>
<template v-if="item.category == 'recommend' || item.category == 'follow'">
<van-swipe vertical lazy-render :show-indicators="false" :loop="false" @change="handleSwipeVertical">
<van-swipe-item v-for="(item2, index2) in item.list" :key="index2">
<!-- ///视频模块 -->
<div class="swipe__video">
<video class="vdplayer" :id="'vd-'+index+'-'+index2" loop preload="auto"
:src="item2.src"
:poster="item2.poster"
webkit-playsinline="true"
x5-video-player-type="h5-page"
x5-video-player-fullscreen="true"
playsinline
@click="handleVideoClicked"
>
</video>
<span v-show="!isPlay" class="btn__play" @click="handleVideoClicked"><i class="iconfont icon-bofang"></i></span>
</div>
<!-- ///信息模块 -->
<div class="swipe__vdinfo flexbox flex-col">
<div class="flexbox flex-alignb">
<!-- ///底部信息栏 -->
<div class="swipe__footbar flex1">
<div v-if="item2.ads" class="item swipe__superlk ads" @click="handleOpenLink(item2)">
<i class="iconfont icon-copylink fs-28"></i>查看详情<i class="iconfont icon-arrR fs-24"></i>
</div>
<div v-if="item2.collectionLs&&item2.collectionLs.length>0" class="item swipe__superlk">
<i class="iconfont icon-copylink fs-24 mr-10"></i><div class="flex1">合集《小鬼当家》主演花絮</div><i class="iconfont icon-arrR fs-24"></i>
</div>
<div class="item uinfo flexbox flex-alignc">
<router-link to="/friend/uhome"><img class="avatar" :src="item2.avatar" /></router-link>
<router-link to="/friend/uhome"><em class="name">{{item2.author}}</em></router-link>
<button class="btn vui__btn vui__btn-primary" :class="item2.isFollow ? 'isfollow' : ''" @click="handleIsFollow(item.category, index2)">{{item2.isFollow ? '已关注' : '关注'}}</button>
</div>
<div class="item at">@{{item2.author}}</div>
<div v-if="item2.topic" class="item kw"><em v-for="(kw,idx) in item2.topic" :key="idx">#{{kw}}</em></div>
<div class="item desc">{{item2.desc}}</div>
</div>
<!-- ///右侧工具栏 -->
<div class="swipe__toolbar">
...
</div>
</div>
</div>
</van-swipe-item>
</van-swipe>
</template>
</van-swipe-item>
</van-swipe>
<!-- ///底部进度条 -->
<div class="swipe__progress"><i class="bar" :style="{'width': vdProgress+'%'}"></i></div>
</div>
</div>
<!-- >>底部TabBar -->
<tabbar
bgcolor="linear-gradient(to bottom, transparent, rgba(0,0,0,.6))"
color="rgba(255,255,255,.6)"
activeColor="#fff"
fixed
/>
<!-- ……商品模板 -->
<v3-popup v-model="isShowGoodsPopup" position="bottom" round xclose title="热销商品" @end="handlePopStateClose" opacity=".2">
<div v-if="goodsLs" class="wrap_goodsList">
...
</div>
</v3-popup>
<!-- ……评论列表模板 -->
<v3-popup v-model="isShowReplyPopup" position="bottom" round xclose opacity=".2">
<div class="nt__commentWrap">
<!-- 评论列表 -->
...
</div>
</v3-popup>
<!-- ……评论编辑器模板 -->
<v3-popup v-model="isShowReplyEditor" position="bottom" opacity=".2">
<div class="vui__footTool nt__commentWrap">
...
</div>
</v3-popup>
<!-- ……分享模板 -->
<v3-popup v-model="isShowSharePopup" anim="footer" type="actionsheet" round xclose opacity=".2"
title="<div style='text-align:left;'>分享至</div>"
:btns="[
{text: '取消', style: 'color:#999;', click: () => isShowSharePopup=false},
]"
>
...
</v3-popup>
</div>
</template>
<script>
/**
* #Desc Vue3.0实现小视频功能
* #Time andy by 2021-02
* #About Q:282310962 wx:xy190310
*/
import { onMounted, onUnmounted, ref, reactive, toRefs, inject, nextTick } from 'vue'
import CmtEditor from '@components/cmtEditor.vue'
import videoJSON from '@/mock/videolist.js'
import emojJSON from '@/mock/cmt-emoj.js'
export default {
components: {
CmtEditor,
},
setup() {
// 定时器
const vdTimer = ref(null)
const tapTimer = ref(null)
const swipeHorizontalRef = ref(null)
const editorRef = ref(null)
const v3popup = inject('v3popup')
const data = reactive({
// ...
})
onMounted(() => {
swipeHorizontalRef.value.swipeTo(data.activeNav, {immediate: true})
// ...
})
// ...
// 垂直切换页面事件
const handleSwipeVertical = (index) => {
if(data.activeNav == 0) {
// 附近页
data.activeOneIdx = index
}else if(data.activeNav == 1) {
// 关注页
data.activeTwoIdx = index
// console.log('关注页索引:' + index)
}else if(data.activeNav == 2) {
// 推荐页
data.activeThreeIdx = index
// console.log('推荐页索引:' + index)
}
vdTimer.value && clearInterval(vdTimer.value)
data.vdProgress = 0
data.isPlay = false
let video = getVideoContext()
if(!video) return
video.pause()
// 重新开始
video.currentTime = 0
data.activeSwipeIndex = index
// 自动播放下一个
handlePlay()
}
// 播放
const handlePlay = () => {
console.log('播放视频...')
let video = getVideoContext()
if(!video) return
video.play()
data.isPlay = true
// 设置进度条
vdTimer.value = setInterval(() => {
handleProgress()
}, 16)
}
// 暂停
const handlePause = () => {
console.log('暂停视频...')
let video = getVideoContext()
if(!video) return
video.pause()
data.isPlay = false
vdTimer.value && clearInterval(vdTimer.value)
}
// 视频点击事件(判断单/双击)
const handleVideoClicked = () => {
console.log('触发视频点击事件...')
tapTimer.value && clearTimeout(tapTimer.value)
data.clickNum++
tapTimer.value = setTimeout(() => {
if(data.clickNum >= 2) {
console.log('双击事件')
}else {
console.log('单击事件')
if(data.isPlay) {
handlePause()
}else {
handlePlay()
}
}
data.clickNum = 0
}, 300)
}
// 播放进度条
const handleProgress = () => {
let video = getVideoContext()
if(!video) return
let curTime = video.currentTime.toFixed(1)
let duration = video.duration.toFixed(1)
data.vdProgress = parseInt((curTime / duration).toFixed(2) * 100)
}
// ...
return {
...toRefs(data),
swipeHorizontalRef,
editorRef,
// ...
}
}
}
</script>
```
另外小视频页底部有一个迷你播放进度条,实时展示当前播放进度。
![](https://oscimg.oschina.net/oscnet/up-f162fad257878603503057ab36da3d4d8c0.png)
ok,使用vue3+vant3开发小视频/直播项目就暂时分享到这里。
![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/035c8d7b00164d8089e6ae790ed48f3f~tplv-k3u1fbpfcp-zoom-1.image)
有疑问加站长微信联系(非本文作者)