简介:这份完整的仿抖音、快手的移动端网页短视频播放源码,定位为一套轻量 H5 短视频交互示例,面向前端初学者、移动端适配开发者和需要快速搭建竖屏播放界面的工程师。压缩包共 53 个文件,以 PHP 后端接口脚本、HTML/CSS 静态页面、JSON/文本配置文件为主,另含 demo.mp4 演示视频、Git 仓库元数据和多张 UI 切图与图标素材,整体约 7.53MB,已有 647 人学习下载。代码覆盖视频列表加载、上下滑动切换、点赞动画、登录请求等典型抖音/快手交互流程,PHP 脚本可模拟后端接口,配合前端 Ajax 请求完整跑通前后端联调,并利用懒加载与 CSS 过渡优化播放体验。目录按页面模板、接口脚本和静态资源分块组织,配有完整图片与字体资源,适合用来学习移动端适配、播放器布局以及小型短视频 Demo 的代码组织方式,也可直接作为课程设计或业务 Demo 的起点。
1. 拆开 zip 先看文件清单:这是 PHP 直出版短视频 H5,不是 React 项目
拿到这个「仿抖音、快手网页版移动端短视频播放源码.zip」,我第一反应是找 package.json 和 webpack 配置。结果压缩包翻了两遍,没有 React 没有 Vue 没有 node_modules,真正的核心文件只有 login.php、get.php、get2.php、get3.php、GET2.json、index.html、pc.html、demo.mp4。这是个用原生 HTML/CSS/JS 加 PHP 搭出来的移动端短视频 H5 壳子,视频直接走 HTML5 video,数据靠 PHP 从 JSON 文件里读出来吐给前端。它适合两类人:一是想快速拿到一套能跑、能改、能放服务器做 demo 的移动端短视频源码,二是想研究上滑下滑切换、预加载、点赞动效这些交互在原生代码里到底怎么写的人。这套包的价值不在框架选型,在交互逻辑本身。
2. 从 zip 内容反推项目结构:静态页、PHP 接口和 mock 数据怎么分工
2.1 三类文件:服务端接口、静态资源、可执行视频
从文件清单能直接看出项目的组织方式。服务端入口是几个 PHP 文件,数据是 JSON/TXT,页面只有两个 HTML,剩下全是图片和视频素材。按职责分组之后,整个项目结构就清楚了:
| 文件 | 归属 | 职责 |
|---|---|---|
| login.php / post.php | 服务端 PHP | 登录入口与点赞上报接口 |
| get.php / get2.php / get3.php | 服务端 PHP | 读取数据源并输出视频列表 |
| GET2.json / GET1.txt | 数据文件 | 视频记录的 mock 数据源 |
| index.html / pc.html | 移动端 / PC 端页面 | 双端入口 |
| css/pc.css | 样式 | 页面主样式 |
| demo.mp4 | 媒体 | 默认演示视频 |
| love.png / loves.png / love1.png | 静态资源 | 点赞相关的三态图标 |
| img/bg.jpg / img/logo.png | 静态资源 | 封面图与品牌图 |
| user.md | 文档 | 用户信息说明 |
这里最值得注意的是.git目录被原样打进了 zip。这种打包方式说明作者直接从仓库目录右键压缩,没有做过滤。对接手的人来说,第一件事应该是打开 GET2.json 看里面的字段结构,它决定了整个信息流渲染成什么样。GET1.txt 和 GET2.json 同时存在,通常是调试时临时换数据文件导致的,具体用哪个,去看 get.php 里file_get_contents指向谁就知道。
2.2 移动端布局与视口配置
index.html 这种移动端页面,最关键的是 viewport、满屏容器和视频容器三个点。viewport 不锁定,Android WebView 里页面宽度会跟随键盘和地址栏变化,视频容器就会出现黑边,所以一般直接写成固定值:
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover"> <link rel="stylesheet" href="css/pc.css">maximum-scale=1加user-scalable=no是短视频 H5 的常见做法:整个交互只有上下滑和点按,没有双指缩放的需求,放开缩放反而容易误触导致画面比例被拉伸。viewport-fit=cover是为了适配 iPhone 的刘海屏和底部 home indicator,让视频画面真正铺满安全区,而不是顶部露一条白边。顺带解释一个细节:样式文件叫 pc.css 不代表只服务于 PC,更可能是作者先做了 PC 版,再把同一套样式复用到移动端,文件名属于历史遗留,别被名字带偏。
2.3 login.php 的登录态与 user.md 的角色
login.php 在这个包里一般承担两种职能之一:静态登录页,或者处理登录请求的接口。因为包里有一份 user.md,我更倾向认为它是用户信息的说明文档,前端可能根据它的字段约定来展示头像和昵称。整套逻辑没有复杂的 session 体系,毕竟短视频 H5 的核心链路是「打开列表 → 看视频 → 点赞 → 继续划」,登录只决定能不能点赞。
post.php 则对应点赞上报这类操作。常见的实现是接收action和video_id两个参数,修改 GET2.json 里面对应的like_count再写回文件。这种设计在演示项目里足够用,放生产环境就得换 Redis 队列或 MySQL。下面这章专门讲数据链路的核心:get.php 怎么把 GET2.json 里的视频列表交给前端。
3. get.php 到 GET2.json:短视频列表的数据流与游标分页
3.1 为什么用 JSON 文件当数据源
这个包里没有数据库文件,也没有 SQL 初始化脚本,数据源就一个 GET2.json。对短视频列表来说,JSON 文件当数据源的好处非常直接:改一个字段,刷新页面就能看到效果,不需要重启 PHP-FPM,更不需要设计表结构。缺点是并发写的时候会丢数据,但列表读取场景根本不写 JSON,只有 post.php 上报时才有这个问题,演示项目完全可以接受。
「开箱即用」和「接近真实」之间需要取平衡。用 JSON 文件是前者,后续想换 MySQL,只要保持 get.php 输出的结构和字段名不变,前端一行都不用改。这个设计思路值得保留:数据源和展示层解耦,接口返回结构才是前后端之间的契约。
3.2 GET2.json 的字段结构
打开 GET2.json 大概率能看到类似这样的记录:
{ "id": 1, "video_url": "demo.mp4", "cover_img": "img/bg.jpg", "nickname": "演示账号", "avatar": "img/logo.png", "like_count": 128, "music_name": "原声", "play_sec": 15 }这些字段覆盖了移动端视频卡片所需的全部信息。video_url是相对路径,相对于 index.html 所在目录解析;cover_img是封面占位图,Android WebView 里视频加载慢很常见,用封面先撑住布局,比黑屏等视频加载体验好得多。play_sec用来显示视频时长。
注意video_url用的是demo.mp4,不是/videos/demo.mp4这种以斜杠开头的绝对路径。原因是项目可能被部署到任意子目录,比如http://host/short-video/index.html,用绝对路径一换目录就 404,相对路径则能跟着部署位置自动适配。这也是这类源码里最常见的部署踩坑点。
3.3 get.php 的参数设计与返回逻辑
get.php 做的事就是读 JSON、按参数过滤、再输出。常见做法是把排序、分页、随机洗牌都堆在一个文件里用分支处理。以短视频 feed 为例,比较合理的参数设计是:
| 参数 | 类型 | 含义 |
|---|---|---|
| seq | int | 当前列表第一条的偏移量 |
| limit | int | 一次返回的视频条数 |
| type | string | list / detail 分支 |
| video_id | int | 只看单条时传的 ID |
对应的 PHP 逻辑可以这样组织,这也是这套源码里最值得拆的一段:
<?php $seq = isset($_GET['seq']) ? intval($_GET['seq']) : 0; $limit = isset($_GET['limit']) ? intval($_GET['limit']) : 6; $type = isset($_GET['type']) ? $_GET['type'] : 'list'; $data = json_decode(file_get_contents('GET2.json'), true); $items = $data['videos'] ?? $data; if ($type === 'detail') { $id = intval($_GET['video_id']); $items = array_values(array_filter($items, function ($v) use ($id) { return $v['id'] === $id; })); } else { $items = array_slice($items, $seq, $limit); } header('Content-Type: application/json; charset=utf-8'); echo json_encode(['code' => 0, 'data' => $items]);逻辑按请求类型分流:列表请求用seq和limit做游标分页。seq不是页码,而是「当前第一条的偏移量」,这样上滑加载和下拉刷新都只需要拿到 count 就能算出下一批数据的位置。array_slice($items, $seq, $limit)直接从偏移位置截取指定长度,超出数组长度时返回空数组。detail分支通过video_id过滤单条,供点击头像或分享页跳转后回显视频用。
这里有个典型坑:array_slice越界后返回空数组,前端如果不判断data.length,滑动时会突然白屏。所以前端 fetch 响应后必须做空列表兜底,无数据时直接复用上一条记录,而不是渲染空节点。
3.4 post.php 点赞上报与 JSON 回写
点赞、关注这类操作在演示项目里通常不需要数据库。post.php 比较务实的写法是接收action=like&video_id=1,读取 GET2.json,对匹配记录做like_count + 1,再file_put_contents写回。这个流程简单,但有明显的并发问题:两个用户同时点赞时,后写的覆盖先写的,计数会丢。演示环境没人并发,可以接受;如果上生产,至少得换成文件锁flock或直接上 Redis。下面给出带文件锁的版本,防止刷新页面时计数被并发请求打飞:
<?php $action = $_GET['action'] ?? 'like'; $videoId = intval($_GET['video_id'] ?? 0); $fp = fopen('GET2.json', 'r+'); if (flock($fp, LOCK_EX)) { $data = json_decode(file_get_contents('GET2.json'), true); $items = $data['videos'] ?? $data; foreach ($items as &$v) { if ($v['id'] === $videoId) { $v['like_count'] = ($action === 'like') ? $v['like_count'] + 1 : max(0, $v['like_count'] - 1); } } unset($v); file_put_contents('GET2.json', json_encode( isset($data['videos']) ? array_merge($data, ['videos' => $items]) : $items, JSON_UNESCAPED_UNICODE )); flock($fp, LOCK_UN); } fclose($fp); header('Content-Type: application/json'); echo json_encode(['code' => 0]);flock(LOCK_EX)是独占写锁,确保同一时刻只有一个请求在修改 JSON。unset($v)是为了断开 foreach 的引用,否则后续循环会修改到同一个元素。回写时用JSON_UNESCAPED_UNICODE,保证中文字段不会变成\uXXXX转义序列,数据可读性更强。这个文件锁方案在演示项目里已经比裸写稳妥得多,唯一注意点是file_get_contents和file_put_contents之间要保证读取的是最新内容,所以整个操作都在锁内完成。
3.5 前端拉取数据的时机与顺序
前端一般在页面加载后立刻请求第一批,等用户滑到倒数第二条的时候再预取下一批。请求用 fetch 写的话是这样:
async function loadVideos(seq, limit) { const url = `get.php?seq=${seq}&limit=${limit}&type=list`; const resp = await fetch(url, { cache: 'no-store' }); const json = await resp.json(); return json.data || []; }cache: 'no-store'在短视频场景下是必须的。WebView 对 GET 请求的缓存策略各有不同,同一个 seq 下拉刷新时可能拿到旧数据,视频 URL 又是一样的,缓存还可能导致视频播的是旧版本。这里同时建议服务端在 get.php 里加上header('Cache-Control: no-store'),前后端双保险。新返回的列表附加到当前 DOM 之后,DOM 数量控制在 12 个以内,超出就移除最顶部的节点,避免长时间滑动后页面节点膨胀导致移动端卡顿,这是移动端性能优化里见效最快的一招。
4. 上滑下滑切换视频:手势判定与移动端 video 播放器状态管理
4.1 为什么短视频 H5 不能直接滚 scroll
移动端做竖屏短视频切换有两种路径:原生滚动容器,每个视频占一个100vh的块;或者用 transform 位移。原生滚动的问题是惯性回弹时 video 跟着滚出视口,播放器状态反复切换,而且滚动容器里的 fixed 元素在不同 WebView 里表现很怪。transform位移的好处是切换动画可控,不会触发滚动相关的浏览器 bug,切换后新旧视频的位置完全由代码决定。
4.2 手势判定:touchstart / touchend 的位移换算
滑动手势只需要关心纵向位移。核心逻辑是在touchstart记录起始 Y,在touchend判断位移方向和距离,超过阈值就切到下一条或上一条,否则回弹。这套源码以原生 JS 为主,直接写 touch 事件比引入 hammer.js 更轻量:
const threshold = 72; let startY = 0; let currentIndex = 0; container.addEventListener('touchstart', (e) => { startY = e.touches[0].clientY; }, { passive: true }); container.addEventListener('touchend', (e) => { const dy = startY - e.changedTouches[0].clientY; if (Math.abs(dy) < threshold) return; const next = currentIndex + (dy > 0 ? 1 : -1); if (next < 0 || next >= totalCount) return; switchTo(next); }, { passive: true }); function switchTo(index) { currentIndex = index; const items = container.querySelectorAll('.feed-item'); items.forEach((item, i) => { item.style.transform = `translateY(${(i - currentIndex) * 100}vh)`; }); refreshPlayer(currentIndex); }threshold取 72px 是因为移动端竖滑时手指的轻微抖动通常小于这个值,太小容易误触,太大则明显感觉「划不动」。72px 在主流 6.1 英寸屏幕上的物理距离接近屏幕高度的 15%,是一个折中值。passive: true让浏览器确认你不调用preventDefault,touch 事件就能在合成器线程上更快响应,这是移动端性能优化里最容易被忽略的一点。translateY按100vh计算而不是写死 px,是防止不同机型视口高度不一致导致位移错位。
4.3 播放器状态管理与 video 置顶问题
切到新视频时要做三件事:新视频调play(),旧视频pause()并重置currentTime = 0,不播放的视频清空src或设置preload="none"。很多实现漏掉最后一步,导致切了十几条后手机发烫、内存暴涨,因为视频解码器一直挂在后台。最省资源的方案是:非当前视频的src置空,当前视频才设置preload="auto",其余都用preload="none"。
搜索引擎里高频的「百度浏览器移动端 video 自动置顶、层级提高问题」,本质是 WebView 把 video 渲染在独立图层上,z-index怎么调都压不住。规避手段是给 video 加playsinline和webkit-playsinline属性:
<video class="feed-video" playsinline webkit-playsinline x5-video-player-type="h5" preload="metadata" poster="img/bg.jpg"> </video>x5-video-player-type="h5"是 Android 端 X5 内核的开关,不加这个,部分国产浏览器会把视频弹成全屏播放器;加上之后视频以内嵌方式渲染,层级问题基本消失。poster属性用封面图占位,避免视频未加载时整个卡片黑底。preload="metadata"只加载视频的时长和第一帧信息,不下载完整视频流,首屏加载成本很低。
4.4 点赞动效与图标资源
包里的 love.png、loves.png、love1.png 分别对应未点赞态、点赞态、飞出的心形。常见的交互是点击按钮时在图片位置叠一个飞出的心形,同时按钮切换为亮色图标。飞出动画用 CSStransform加opacity组合即可:
.love-burst { animation: loveBurst 0.6s ease-out forwards; } @keyframes loveBurst { 0% { transform: scale(0.3); opacity: 1; } 100% { transform: scale(1.6) translateY(-30px); opacity: 0; } }这段动画只操作 transform 和 opacity,两个属性都走合成器,不触发 layout 和 paint,稳定跑 60fps。如果改成动画 top/left,每一帧都会触发重排,在中低端 Android 机型上就是肉眼可见的卡顿。点赞数字的前端更新采用乐观策略:先加一,异步请求 post.php,失败再回滚,交互响应速度更快,用户不会感觉到网络请求的等待。
5. 部署验证:php -S 跑起来之后的三个高频坑
5.1 启动命令与目录规范
把 zip 解压后,目录路径尽量不要带空格和中文。本地调试直接用 PHP 内置服务器,在解压目录内执行:
php -S 0.0.0.0:8080然后浏览器访问http://localhost:8080/index.html。用0.0.0.0而不是127.0.0.1,是为了方便手机在同一局域网内通过电脑 IP 直接访问调试页面,而不是只能看 PC 模拟器效果。如果解压目录名带空格,PHP 内置服务器会把路径解析错,访问页面直接 404。
5.2 高频问题排查表
| 现象 | 原因 | 处理 |
|---|---|---|
| 视频黑屏但音频正常 | 移动端自动播放被浏览器拦截 | 播放必须发生在用户手势事件回调里 |
| get.php 返回 200 但前端拿到空数组 | seq 超出数组长度 | 前端判断 data.length,空则复用上一条 |
| 刷新后点赞数丢失 | post.php 写的是请求时刻的 JSON | 属于预期行为,演示数据不保证持久化 |
| 手机上打开出现 PC 版样式 | 响应式断点或 UA 跳转配置不对 | 检查 index.html 头部是否有移动端专用 meta |
| video 层级盖住弹窗 | 缺少 playsinline / x5 内核属性 | 按 4.3 给 video 补全内嵌播放属性 |
5.3 最实用的验证技巧:直接用 URL 参数测分页
把 GET2.json 里的数组复制到 10 条以上,然后在浏览器地址栏手动访问:
http://localhost:8080/get.php?seq=0&limit=2&type=list http://localhost:8080/get.php?seq=6&limit=2&type=list第一次返回前两条记录,第二次返回第 7、8 条记录。用这种方式可以 30 秒内确认游标分页是否正常,把接口层和前端渲染层切开验证,避免在页面里反复滑动排查到底是手势问题还是数据问题。
本文还有配套的精品资源,点击获取