时间:2022-11-04 09:52:39 | 栏目:vue | 点击:次
年前单位需要搞一个类似抖音的需求,这本应是客户端的任务,然而,不知天高地厚的我却接了下来,然而下细致调研之下,发现网上并没有成熟的方案,但是却又很多需求,各大论坛全是提问的帖子,却少有人回答和解决。
这一瞬间,俺慌了,毕竟单位的活,排期都是定死的,这时候临阵退缩,实乃下下策。于是只能撸起袖子加油干。毕竟自己揽的事,含着泪也要干完,这就是男人,一个吐沫一个钉!
大家知道,web端比起客户端的劣势有几点,想要做出类似客户端的复杂的交互效果,需要考虑几个问题:
而在我调研了抖音的web端、git上的一些开源的相关项目、以及一些零零散散的回答之后,发现都不太匹配 他们在实现上,那么只能集几百家之长自己来了,既然自己来就需要针对当前三个问题来寻找既能解决问题,又能快速实现的方案(毕竟有排期)
在实现的初步设想中,我们不只需要解决问题,其实也需要考虑一些架构设计,也就是你怎样去将关注度分离,怎样将组件的颗粒度拆的细致,能将每一个组件独立出来,外部单独引用,怎样将每一个组件做通用,方便日后维护,并且还能快速开发,不耽误排期,这其实就是你在这做也无需求之初需要去想的一些问题,总结如下
组件设计的设想俺才疏学浅也就能想到这了,接下来就该解决在调研中发现的三个问题:
工程构建为了装逼上了最新的vite ,体验了一把,开发体验确实是丝滑快速。由于vite天生支持库的开发,只需要在vite.config.ts 添加build内容即可
build: { lib: { entry: path.resolve(__dirname, 'src/components/index.ts'), name: 'videoSlide', fileName: (format) => `index.${format}.js` }, rollupOptions: { // 确保外部化处理那些你不想打包进库的依赖 external: ['vue'], output: { // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量 globals: { vue: 'Vue' } } } },
由于库可能给ts大佬使用,需要安装vite-plugin-dts 插件,来生成d.ts文件
由于视频内容和轮播部分的处理是两个独立的逻辑,所以将代码拆分为两个组件video.vue以及slide.vue
video的实现的基本思路就是重写原生video 标签默认ui来达到自定义的目的,样式就不在赘述,主要就是video提供的一些事件重写video默认行为,这里简述下重点的函数
// vue <video playsinline="true" webkit-playsinline="true" mediatype="video" :poster="poster" @progress="progress" @durationchange="durationchange" @loadeddata="loadeddata" @playing="playing" @waiting="waiting" @timeupdate="timeupdate" @canplay="playing" @ended="ended" > <source :src="src" type="video/mp4" /> </video> //js setup({ autoplay }) { // 是否是暂停状态 const paused = ref(true); // 视频总时间 const endTime = ref(second(0)); //播放的时间 const startTime = ref(second(0)); // 是否是按下状态 const isPress = ref(false); //缓冲进度 const percentageBuffer = ref(0); // 播放进度 const percentage = ref(0); // 保存计算后的播放时间 const calculationTime = ref(0); // 拿到video 实例 const video = ref(null); // 是否展示封面图 const showImg = ref(true); // 是否处于缓冲中 const loading = ref(false); // 播放 function play() { video.value.play(); paused.value = false; } // 暂停 function pause() { if (paused.value) return; video.value.pause(); paused.value = true; loading.value = false; } // 获取缓冲进度 function progress() { if (!video.value) return; percentageBuffer.value = Math.floor( (video.value.buffered.length ? video.value.buffered.end(video.value.buffered.length - 1) / video.value.duration : 0) * 100 ); } // 时间改变 function durationchange() { endTime.value = second(video.value.duration); console.log("时间改变触发"); } // 首帧加载触发,为了获取视频时长 function loadeddata() { console.log("首帧渲染触发"); showImg.value = false; autoplay && play(); } //当播放准备开始时(之前被暂停或者由于数据缺乏被暂缓)被触发 function playing() { console.log("缓冲结束"); loading.value = false; } //缓冲的时候触发 function waiting() { console.log("处于缓冲中"); loading.value = true; } // 时间改变触发 function timeupdate() { // 如果是按下状态不能走进度,表示需要执行拖动 if (isPress.value || !video.value) return; startTime.value = second(Math.floor(video.value.currentTime)); percentage.value = Math.floor( (video.value.currentTime / video.value.duration) * 100 ); } // 按下开始触发 function touchstart() { isPress.value = true; } //松开按钮触发 function touchend() { isPress.value = false; video.value.currentTime = calculationTime.value; } // 拖动的时候触发 function touchmove(e) { const width = window.screen.width; const tx = e.clientX || e.changedTouches[0].clientX; if (tx < 0 || tx > width) { return; } calculationTime.value = video.value.duration * (tx / width); startTime.value = second(Math.floor(calculationTime.value)); percentage.value = Math.floor((tx / width) * 100); } //点击进度条触发 function handleProgress(e) { touchmove(e); touchend(); } // 播放结束时触发 function ended() { play(); } onMounted(() => {}); return { video, paused, pause, play, progress, durationchange, loadeddata, endTime, startTime, playing, percentage, waiting, timeupdate, percentageBuffer, touchstart, touchend, touchmove, isPress, ended, handleProgress, loading, showImg, }; },
需要注意的是,需要自定义内容交给了使用者去自定义,全部通过插槽传入当前组件,这样就方便了根据内容自定义样式了
slide.vue 就是处理滑动内容的组件,他包含了常用的上拉刷新,预加载等内容核心代码如下:
// vue <swiper direction="vertical" @transitionStart="transitionStart" > <swiper-slide class="slide-box" v-for="(item, index) in list" :key="index"> <slot :item="item" :index="index" :activeIndex="activeIndex" v-if="activeIndex >= index - 1 && activeIndex <= index + 1" ></slot> </swiper-slide> </swiper> //js setup({ list }, { emit }) { const activeIndex = ref(0); function transitionStart(swiper) { //表示没有滑动,不做处理 if (activeIndex.value === swiper.activeIndex) { // 表示是第一个轮播图 if (swiper.swipeDirection === "prev" && swiper.activeIndex === 0) { // 表示上拉刷新 emit("refresh"); } else if ( swiper.swipeDirection === "next" && swiper.activeIndex === list.length - 1 ) { // 滑动到底部 emit("toBottom"); } } else { activeIndex.value = swiper.activeIndex; // 为了预加载视频,提前load 数据 if (swiper.activeIndex === list.length - 1) { emit("load"); } } } return { transitionStart, activeIndex, }; },
需要注意的是有两点
为了预加载数据会在滑动到最后一帧的时候去请求数据,但是由于请求是异步的,如果在滑动到最后一个视频的时候在快速下滑会触发滑动到底部的事件,这时候其实新数据请求回来之后便又不是底部了,这时候则需要你去做个判断,如果正在请求中滑动到底部不去处理你的逻辑
为了性能考虑,只渲染了active 、prev、next内容,其他一律渲染空节点,并且为了防止页面中出现多个vidoe标签,prev 和next 只渲染默认图内容
组合使用其实就非常简单了:
//vue <Yslide :list="data" v-slot="{ item, index, activeIndex }" @refresh="refresh" @toBottom="toBottom" @load="load" > <Yvideo :src="item.entStoreVO.video" :poster="item.entStoreVO.videoImg" :index="index" :activeIndex="activeIndex" autoplay > <div class="mantle"> <div class="right" @click.stop=""> <div class="right-btn fabulous" @click="fabulous">点赞</div> <div class="right-btn comment" @click="comment">评论</div> <div class="right-btn collection" @click="collection">收藏</div> <div class="right-btn share" @click="share">分享</div> </div> </div> </Yvideo> </Yslide>
在组合使用中,我将video通过插槽的方式传入silide内部,这样做的原因是,为了用户能自定义传入内容,这也是很多插件库惯用的伎俩,增加了组件的灵活性,又增加了组件的独立性
在web浏览器中你经常会看到DOMException: play() failed because the user didn't interact with the document first 这个问题,
首先可以肯定的是在web浏览器中在与浏览器没有交互的情况下是不允许自动播放的,目前暂时还无法突破这个限制
如果你要嵌入app中,webview 可以突破,具体方法大家可自行查询,网上教程数不胜数。
将插件地址奉上,供大佬们参考,如有需求可直接引用,也可,克隆下来自行修改,如有问题请提issues github.com/yixinagqing…