项目介绍
基于之前原生开发网站的二次开发。 由于短剧的竖屏播放,并不适合h5,所以全面模仿抖音
API
仅有短剧相关、分类的部分 使用Apifox悦享好剧
配色信息
整体颜色采用#e84a5f 辅助以#fecea8

是#323232灰色 纯白和纯黑
图标

项目结构
我的页
就照这个抄,用上ElementPlus UI吧,不想手撸了
图标采用el-icon
- 头像
2. 顶部个人信息区域
- 抖音号:用
el-text显示数字,加粗样式。 - 昵称和标签:
- 昵称“能不能早点退休”用
el-title(或h1)显示。 - 年龄、地点、学校等信息用
el-text组合,字体较小。 - “添加性别等标签”用
el-button(类型为text)表示可点击。
- 昵称“能不能早点退休”用
3. 数字统计行(获赞/朋友/关注/粉丝)
- 使用
el-row和el-col创建4列网格布局- 每个数字用
el-statistic组件突出显示。 - 下方标签用
el-text小字号描述。
- 每个数字用
4. 功能入口(抖音商城/观看历史/等)
- 同样用
el-row和el-col网格布局,每列包含:- 图标:使用
el-icon(如购物车、历史记录等)。 - 文字:用
el-text描述。
- 图标:使用
- “查看更多”用
el-button(链接样式)放在末尾。
5. 选项卡(作品/私密/收藏/喜欢)
- 使用
el-tabs组件,切换不同标签页。 - “作品”页内容:
- 空状态提示:用
el-empty组件显示“发作品,留下记忆”。 - “去发布”按钮:用
el-button(主要样式)引导用户操作。
- 空状态提示:用
初始请求整个视频(不跳转进度)
当你点击一个视频链接时,会发生以下步骤:
- 发起请求:你的浏览器或视频播放器(客户端)向服务器发送一个标准的
HTTP GET请求,请求那个.mp4文件的 URL。 - 服务器响应:服务器接收到请求后,会准备发送整个文件。在发送数据之前,它会在响应头(Response Headers)中包含一些重要信息:
HTTP/1.1 200 OK:状态码 200 表示成功,并会发送整个资源。Content-Type: video/mp4:告知客户端这是一个 MP4 视频文件。Content-Length: 123456789:告知客户端这个视频文件的总大小(例如 123MB)。Accept-Ranges: bytes:这是最关键的头之一。它告诉客户端,本服务器支持按字节范围请求(Range Requests)功能。这是实现进度跳转的基础。
- 开始下载:之后,服务器开始将视频文件的字节流通过 TCP 连接发送给客户端。
- 播放开始:播放器通常会先下载一小部分数据(缓冲),然后就开始播放。同时,它会在后台继续下载剩余的视频文件。
在这种情况下,服务器只是像一个普通的文件服务器一样,从头到尾、线性地发送整个文件。它不知道客户端是播放了、暂停了还是关闭了页面。
当你跳转视频进度时(例如拖拽进度条)
这是最有趣的部分。这时,HTTP 范围请求(Range Requests) 机制就开始工作了。
- 用户操作:你将进度条从
00:00拖到05:00。 - 客户端计算:播放器知道视频的总长度(从之前的
Content-Length获知),它会计算出05:00这个时间点对应的是文件中的 approximately 哪个字节位置。(例如,视频总长 10分钟,对应 100MB 文件,那么 5分钟 就大约在 50MB 的位置)。 - 发送范围请求:播放器中止当前的文件下载(可能会关闭之前的 TCP 连接),然后向同一个 URL 发起一个新的
HTTP GET请求。这个请求包含了一个特殊的请求头:Range: bytes=52428800-- 这个例子表示“请从文件的第 52428800 个字节(即 50MB)开始发送,直到文件结束”。
- 你也可以请求一个特定范围,如
Range: bytes=52428800-57671680(请求 50MB 到 55MB 之间的数据)。
- 服务器处理范围请求:服务器看到
Range头后,就不会发送整个文件了。而是:- 在响应头中发送状态码
HTTP/1.1 206 Partial Content(206 表示“部分内容”)。 - 发送头
Content-Range: bytes 52428800-104857599/104857600(表示“本次发送的是从 52428800 到 104857599 字节的内容,文件总大小是 104857600 字节”)。 - 只发送客户端请求的那一部分字节数据。
- 在响应头中发送状态码
- 客户端接收并播放:播放器收到从 5分钟 开始的数据包,立即解码并播放。同时,它可能会继续请求并缓存之后的数据。
总结一下跳转过程:服务器通过接收客户端请求中的 Range 头来知道客户端需要哪一部分数据。服务器本身不记录也不关心你的播放进度,它只是根据客户端的指令“精准投递”数据块。
关键技术点与类比
服务器如何知道进度?
服务器不知道,也不需要知道。 是客户端主动在请求头(Range header)中明确告诉了服务器“我需要从第X字节开始的数据”。服务器就像一个仓库管理员,客户端是提货员。提货员说“我要从仓库中间第500箱货开始搬”,管理员就照做,但他并不关心提货员为什么从中间开始搬,也不记录提货员的进度。
为什么可以这么快?
视频文件可以被快速跳转,是因为 MP4 等现代容器格式在文件末尾(或开头)有一个叫做 “元数据索引”或“MOOV Atom” 的结构。这个索引就像一本书的目录,记录了整个视频的关键帧、时间戳和对应字节位置的关系。播放器在初始请求时可能会先下载这个“目录”(如果它不在文件开头,有时需要先加载整个文件末尾),这样当你拖拽进度条时,它能立刻查表得知目标时间点对应的精确字节位置,从而发出准确的 Range 请求。
现代优化:流媒体协议(HLS/DASH)
对于非常长的视频(如电影)或直播,使用普通的 HTTP 范围请求来跳转效率可能不高。因此,像 HLS (HTTP Live Streaming) 或 DASH (Dynamic Adaptive Streaming over HTTP) 这样的现代流媒体协议更为常见。 它们的工作原理是:
- 将一个大视频文件切割成成百上千个小的.ts 或 .m4s 视频片段(通常每个只有几秒钟)。
- 创建一个 .m3u8 或 .mpd 播放列表(清单)文件,这个文件包含了所有这些小片段的 URL 和信息。
- 客户端首先下载播放列表,然后根据当前网速和播放进度,逐个请求下载这些小片段文件。 当你跳转时,播放器直接计算你需要哪个片段(Segment),然后去请求那个对应的小文件即可,变得更加高效和灵活。
从响应头我们可以读出以下关键信息:
Status Code: 206 Partial Content: 这不是200 OK。服务器没有返回整个文件,而是返回了文件的一部分。这明确表示服务器支持 HTTP 范围请求(Range Requests)。Content-Range: bytes 0-13052203/13052204: 这告诉我们,服务器返回的是从第 0 个字节到第 13,052,203 个字节的内容,而整个文件的大小是 13,052,204 个字节。也就是说,它几乎返回了整个文件,但是以“部分内容”的形式返回的。Accept-Ranges: bytes(虽然在你的列表里没明确写,但206响应必然意味着服务器支持): 这是服务器宣告自己支持范围请求的标志。- 请求头中的
Range: bytes=0-: 这是浏览器发出的指令,意思是“请从第0字节开始发送,一直到结束”。这是为了先获取整个文件的一部分(通常是元数据)以开始播放。
结论:你的服务器完全具备视频流式传输的基础能力! 它不是一个简单的“文件服务器”,而是一个支持按需交付的“媒体服务器”。
既然服务器支持 Range Requests,我们就可以实现伪流媒体(Pseudo-Streaming)。这意味着我们虽然不能像 HLS 那样动态切换码率,但可以实现几乎所有其他优秀的用户体验:
- 快速启动(Fast Startup): 播放器不需要下载整个文件,只需要下载足够播放几秒钟的初始数据就可以开始播放。
- 高效跳转(Efficient Seeking): 当你拖拽进度条时,播放器会计算对应的时间点并发送一个新的
Range请求(例如Range: bytes=5242880-),服务器会精准地返回从那个字节位置开始的数据,而不需要下载前面的所有内容。 - 带宽节约(Bandwidth Saving): 如果用户提前划走,下载会中止,节省了流量。
你的技术选型可以更加大胆和高效。
方案一:使用功能更强的播放器库(强烈推荐)
即使直接播放 .mp4,使用像 video.js 这样的库也能带来巨大好处:
- 统一UI: 提供美观、可定制且统一的控制栏UI,跨浏览器体验一致。
- 强大API: 提供了更简单易用的 API 来控制播放、全屏、音量等。
- 生态插件: 有丰富的插件生态。
- 更好的兼容性: 它封装了不同浏览器下
Range Requests等行为的差异。
集成示例(Video.js):
<head>
<link href="https://vjs.zencdn.net/8.10.0/video-js.css" rel="stylesheet" />
</head>
<body>
<video
id="my-video"
class="video-js vjs-default-skin vjs-big-play-centered"
controls
preload="auto"
muted
playsinline
width="100%"
height="100vh"
style="object-fit: cover;"
data-setup='{}'
>
<source src="https://playletcdn.nnchenxin.cn/video/jaxczcqrgdq/9.mp4" type="video/mp4" />
</video>
<script src="https://vjs.zencdn.net/8.10.0/video.min.js"></script>
</body>方案二:坚持使用原生 Video 标签,但优化预加载策略
对于“抖音式”的滑动体验,预加载策略至关重要。
- 当前视频: 正常播放,
preload="auto"。 - 下一个视频: 创建一个隐藏的
video元素进行预加载。js// 伪代码:预加载下一个视频 function preloadNextVideo(url) { const preloadVideo = document.createElement('video'); preloadVideo.src = url; preloadVideo.preload = 'auto'; preloadVideo.muted = true; preloadVideo.style.display = 'none'; document.body.appendChild(preloadVideo); // 预加载一段时间后或一定数据量后,移除它以节省资源 setTimeout(() => { document.body.removeChild(preloadVideo); }, 5000); // 预加载5秒 } - 懒加载: 离开视口的视频应立即
pause()并将其src置空或替换为一个极小的预览视频,以释放内存和网络资源。当它再次进入视口时,再重新设置src。
针对你提供的接口数据的实现策略
你的接口返回的是一个分页的视频列表。实现上下滑动切换的关键如下:
容器与手势:
- 创建一个全屏的容器,使用 CSS 的
overflow: hidden和touch-action: none。 - 使用
touchstart,touchmove,touchend事件监听手势,计算滑动距离和方向。 - 通过 CSS
transform: translateY来动画地移动整个视频列表。
- 创建一个全屏的容器,使用 CSS 的
视频管理:
- 同时只存在 3 个
video元素:当前播放的、上一个(用于回滑)、下一个(用于预加载和下滑)。 - 根据滑动的索引,动态更新这三个视频的
src。 - 非活跃的视频必须
pause()并且将其currentTime重置为 0。
- 同时只存在 3 个
状态同步:
- 当滑动结束时,播放新的活跃视频:
activeVideo.play().catch(handleError); - 暂停并重置上一个非活跃视频:
inactiveVideo.pause(); inactiveVideo.currentTime = 0;行动路线:
- 当滑动结束时,播放新的活跃视频:
首先,用
video.js或原生视频标签测试你的视频URL,随意拖拽进度条,确认seek体验是流畅的。这将给你巨大信心。然后,集中精力实现上下滑动的容器组件。这是用户体验的基石。
接着,实现 “三视频实例”模型,这是性能优化的核心。确保内存和网络资源不被浪费。
最后,打磨细节,如加载动画、错误处理(网络中断、视频加载失败)、播放完成后的循环等。
你现在完全有能力打造一个体验非常接近原生抖音的Web应用了。你的服务器已经提供了所需的基础设施,剩下的就是前端如何巧妙地利用它。祝你成功!
非常好的问题!这涉及到前端视频播放的技术演进和工程化实践。我们来逐一深入分析。
Video.js vs 原生 Video 标签
你可以把原生 <video> 标签理解为浏览器提供的“基础款”播放器,而 Video.js 则是在这个基础款之上包装的“豪华旗舰款”UI组件和工具库。
| 特性 | 原生 <video> 标签 | Video.js |
|---|---|---|
| 核心本质 | 浏览器内置的HTML元素。 | 一个基于JavaScript的开源库,其底层最终仍然是创建一个原生 <video> 标签。 |
| UI(用户界面) | 不同浏览器(Chrome, Safari, Firefox, Edge)有完全不同且不可定制的控制栏样式。体验不统一。 | 提供一套完全统一、可定制的HTML/CSS皮肤。你可以改变颜色、布局、按钮等,确保所有浏览器体验一致。 |
| 功能与API | 提供基础的API(play(), pause(), currentTime等)。 | 扩展了大量易用的API和事件,提供了更强大、更直观的控制方式(如插件系统、标准化的事件监听)。 |
| 兼容性 | 兼容所有现代浏览器,但不同浏览器对视频格式(如HLS)的支持程度不同。 | 填补了浏览器间的格式支持差距。例如,通过在不支持HLS的浏览器(如Chrome)中自动引入并启用 hls.js 库来播放 .m3u8 流。 |
| 流媒体支持 | 原生支持程度因浏览器而异(Safari原生支持HLS,Chrome等不支持)。 | 通过插件机制,可以统一处理多种流媒体协议(HLS, DASH),提供一致的开发体验。 |
| 可访问性 | 基础的可访问性支持。 | 提供更完善的ARIA支持,对屏幕阅读器等辅助工具更友好。 |
| 体积 | 0,无需加载。 | 需要引入额外的CSS和JS文件(可使用CDN),体积大约几十KB(gzip后)。 |
结论:原生标签是“内核”,Video.js 是“内核+皮肤+增强功能包”。
Video.js 的具体优化实现方式
Video.js 并非魔法,它的优化是基于对原生能力的封装和增强。
UI 统一与定制
- 如何实现:Video.js 在初始化时,会隐藏原生的控制栏,然后在视频容器外包裹一层自己用
HTML和CSS编写的div结构,再根据播放器的事件(如play,pause,timeupdate)来更新自己UI的状态。 - 优化点:开发者无需关心浏览器差异,只需通过配置即可修改UI,实现了“一次开发,到处一致”。
- 如何实现:Video.js 在初始化时,会隐藏原生的控制栏,然后在视频容器外包裹一层自己用
格式与协议兼容性(最重要的优化)
- 场景:你的服务器返回的是
.mp4,但假设以后支持了.m3u8(HLS)。 - 原生问题:在 Chrome 中直接放
.m3u8地址是无法播放的。 - Video.js 方案:
- 它通过
source的type属性(如application/x-mpegURL)来判断视频类型。 - 如果检测到是 HLS 流且浏览器不支持,它会自动检测是否引入了
hls.js库。如果引入了,就 silently 使用hls.js来解码和播放视频,对开发者无感。 - 代码示例:html
<video-js data-setup='{}'> <source src="https://example.com/video.m3u8" type="application/x-mpegURL"> </video-js>- 在 Safari 中:使用原生
video标签的能力播放。 - 在 Chrome 中:自动使用
hls.js来播放。
- 在 Safari 中:使用原生
- 优化点:开发者只需关心提供正确的视频地址和类型,跨浏览器的兼容问题由 Video.js 解决。
- 它通过
- 场景:你的服务器返回的是
强大的插件生态系统
- 实现:Video.js 提供了完善的插件开发机制。你可以找到用于弹幕、水印、缩略图预览、质量选择器等的插件。
- 优化点:无需重复造轮子,使用社区插件可以快速为播放器添加复杂功能。
更友好、健壮的事件和API处理
- 实现:它对原生的事件和API进行了封装,提供了更简单、一致的编程接口,并处理了很多边界情况(错误处理、自动重试等)。
没有 <video> 标签的时代:史前时期
在 HTML5 和 <video> 标签标准化之前(约2009年之前),网页播放视频主要依赖浏览器插件。
Adobe Flash Player(最主流)
- 工作原理:网页中嵌入一个
<object>或<embed>标签,指向一个.swf文件(Flash影片)。这个SWF文件内部包含一个视频播放器UI和逻辑,它通过网络加载.flv(Flash Video)或后来的.mp4文件进行播放。 - 流程:
网页 -> Flash插件 -> FLV/MP4视频 - 缺点:
- 性能差:作为插件运行,消耗大量CPU和内存。
- 安全性:Flash是著名的安全漏洞重灾区。
- 兼容性:用户必须手动安装插件,且在iOS设备上被完全禁止。
- SEO不友好:视频内容对搜索引擎不可见。
- 工作原理:网页中嵌入一个
Microsoft Silverlight
- 类似Flash,是微软推出的竞争对手,主要用于一些特定场景(如Netflix早期就使用Silverlight)。
QuickTime, RealPlayer, Windows Media Player
- 更早的方案,通过在页面中嵌入这些播放器的ActiveX控件(IE)或插件(Netscape)来播放特定格式的视频(
.mov,.rm,.wmv)。体验非常碎片化。
- 更早的方案,通过在页面中嵌入这些播放器的ActiveX控件(IE)或插件(Netscape)来播放特定格式的视频(
那个时代的“播放”按钮,点击后通常会启动一个外部程序或在一个新的插件窗口内播放,体验与网页本身是割裂的。
好的,我来详细解释一下你提到的两个概念:nextTick 和 :ref="(el) => (episodeRefs[item.eid] = el)",以及它们在 Vue 3 中的作用和用法。
1️⃣ nextTick
定义
nextTick 是 Vue 提供的一个工具函数,用来 在 DOM 更新完成后执行某段代码。
在 Vue 的响应式系统中,state 改变不会立即更新 DOM,而是会异步批量更新。
如果你直接在修改数据后操作 DOM,可能会拿不到最新的 DOM 元素。
nextTick 就是解决这个问题的办法。
用法示例
import { ref, nextTick } from 'vue'
const count = ref(0)
const box = ref(null)
function increment() {
count.value++
// DOM 还没更新
console.log(box.value) // 可能还是旧的 DOM
// 在 DOM 更新完成后执行
nextTick(() => {
console.log(box.value) // DOM 已经更新,可以安全操作
})
}在你的场景中
nextTick(() => {
scrollToLastEpisode()
})allEpisodeList是异步请求获取的,更新后 DOM 才会生成。nextTick确保 DOM 渲染完成,再调用scrollToLastEpisode()滚动到最后播放位置。
2️⃣ :ref="(el) => (episodeRefs[item.eid] = el)"
定义
这是 函数式 ref 的写法,用来把模板中的 DOM 元素 动态保存到对象或数组中。
普通 ref:
<div ref="box"></div>- 绑定到单个 DOM 元素,
box.value就是该 DOM。
函数式 ref:
<div :ref="el => elements[id] = el"></div>可以把 DOM 保存到对象或数组中,用
id或index作为 key。el参数就是当前 DOM 元素。用法常见于
v-for循环时,需要保存 多个 DOM 引用。
在你的场景中
<div
v-for="item in allEpisodeList"
:key="item.url"
:ref="el => (episodeRefs[item.eid] = el)"
>
{{ item.episode }}
</div>episodeRefs是一个对象:{}每次循环生成一集的 DOM 元素时,把它存入
episodeRefs对应的eid属性:
episodeRefs = {
11650: <div>...</div>,
11651: <div>...</div>,
...
}- 这样你就可以通过
episodeRefs[eid]直接获取对应 DOM 元素,用于滚动或高亮操作。
小结
普通 ref:绑定单个 DOM 元素
函数式 ref:动态绑定多个 DOM 元素,常用于
v-for或动态列表
💡 组合起来的作用
使用
:ref="el => (episodeRefs[item.eid] = el)"保存每集 DOM。使用
nextTick等待 DOM 渲染完成。然后通过
episodeRefs[lastEid].scrollIntoView()滚动到最后播放的位置。
如果你需要,我可以画一张 图示说明函数式 ref + nextTick 的流程,让你更直观理解 Vue 渲染和滚动逻辑。
你希望我画吗?