当前位置: 首页 > news >正文

Video.js 视频列表插件:点选即播,自动续播下一个

本文还有配套的精品资源,点击获取

简介:一个轻量、即用型的 Video.js 视频列表组件,实现单页内多视频缩略图/标题展示与交互控制。用户点击任意列表项,页面立即加载对应视频并开始播放;当前视频结束或手动切换时,可无缝跳转至下一个视频。资源包内置完整运行环境:包含 index.html 入口页、video.min.js(Video.js 7.x 核心)、video-js.css 及压缩版样式文件、jquery.min.js(用于 DOM 操作兼容)、示例截图和纯文本说明文档。目录结构清晰划分 images(存放封面图)、statics(静态资源)、lang(预留多语言支持)、videojs_list(主逻辑代码),方便后续扩展图标、字幕、地区化配置。所有功能基于原生 JavaScript 封装,不依赖 Webpack/Vite 等构建工具,直接引入 HTML 即可运行,适配现有网站嵌入或快速搭建视频聚合页。

1. 项目概述:为什么一个“点选即播+自动续播”的视频列表,值得单独封装成插件?

在做视频聚合页、课程学习平台、产品演示站或者内部知识库时,我遇到过太多次这样的场景:产品经理甩来一张设计稿,上面是横向滚动的封面图流,配着“点击就播”“播完自动切下一个”“支持手机端手势滑动切换”的需求;前端同事打开 Figma,眉头一皱:“这不就是个 ul li 列表加 video 标签?写个 for 循环不就完了?”——结果三天后,页面卡顿、缩略图错位、iOS 上自动播放被拦截、视频结束事件监听失效、切换时黑屏半秒、甚至用户点错两次导致两个视频同时加载……最后不是回滚到原生 video 标签硬写,就是临时扒一段 jQuery 插件凑合,代码散落在三个文件里,连注释都写着“此处待重构”。

这就是为什么我花了整整两周时间,把这套 Video.js 视频列表功能,从零打磨成一个真正开箱即用的插件。它不是简单地把几个视频塞进 div,而是围绕Video.js 的生命周期、事件流与状态管理机制,构建了一套轻量但完整的控制闭环。核心就三件事:列表项可点击、点击即加载并播放、当前视频结束/中断时,自动触发下一个的加载与播放。没有花哨的动画,不依赖任何构建工具,所有逻辑压缩在不到 400 行原生 JS 里(不含 Video.js 和 jQuery),却覆盖了真实项目中 95% 的边界情况。

关键词里的 “videojs插件” 不是噱头——它严格遵循 Video.js 官方插件开发规范,通过videojs.registerPlugin()注册,支持player.list({ videos: [...] })这样的链式调用;“视频列表组件” 指的是它提供了一套独立 DOM 结构(带 class 命名空间)、可复用 CSS 样式(含响应式断点)和语义化 HTML 模板;而“自动续播”更是经过 iOS/Android/Chrome/Firefox/Safari 全平台实测验证的可靠行为,不是靠ended事件粗暴跳转,而是结合canplaythroughloadedmetadatatimeupdate多事件协同判断,确保下一个视频在上一个结束前 300ms 就已缓冲就绪,真正做到“无缝”。

它适合谁?如果你正在维护一个老系统,不能引入 Vue 或 React;如果你要给客户快速搭一个视频介绍页,没时间配 Webpack;如果你的团队里有实习生,需要一份“改两行配置就能上线”的方案——那它就是为你写的。不是最炫的,但一定是最稳的。我把它放在生产环境跑了 8 个月,日均 UV 2.3 万,没收到一例关于“播着播着卡住”或“点完没反应”的反馈。下面,我就带你一层层拆开这个包,看看它怎么做到的。

2. 整体架构与设计思路:为什么不用 Vue/React?为什么坚持原生 JS + Video.js 插件模式?

2.1 放弃框架的理由:轻量性与嵌入自由度是第一优先级

很多人看到“视频列表”,第一反应是:“这不得上个 Vue 组件?v-for 渲染列表,v-model 绑定当前索引,watch 监听 ended 事件……” 理论上没错,但现实很骨感。我做过对比测试:一个纯 Vue 3 的视频列表组件(含 Composition API + Pinia 状态管理),打包后最小体积 86KB(gzip 后),首次渲染需等待 Vue runtime 加载、解析、挂载,再执行组件逻辑;而本插件整个videojs-list.min.js(含核心逻辑+默认样式注入)仅 12.7KB,且在<script>标签里defer加载后,DOM Ready 时即可调用,无需等待任何框架初始化。

更重要的是嵌入自由度。我们服务的客户里,有政府单位的内网系统(只允许 IE11+,禁用 npm)、有制造业的 MES 页面(运行在 WinCE 设备的定制浏览器里)、还有教育机构的老课件平台(HTML 页面直接 FTP 上传,不允许任何构建步骤)。这些场景下,“npm install videojs-list && import { ListPlugin } from ‘videojs-list’” 是天方夜谭。而本方案只需三步:
1. 把videojs_list/文件夹整个拷进你项目的static/目录;
2. 在 HTML<head>里按顺序引入video-js.cssjquery.min.jsvideo.min.jsvideojs-list.min.js
3. 在<body>底部写一行初始化代码。

全程无构建、无编译、无依赖冲突。jQuery 的引入不是为了炫技,而是为了解决一个残酷事实:IE11 下document.querySelector对某些动态生成的 class 名支持不稳定,而$(selector)在 jQuery 3.6.0 中已针对此做了兼容补丁。这不是技术债,是向现实妥协的务实选择。

2.2 插件模式的核心优势:与 Video.js 生命周期深度绑定

Video.js 不是一个简单的播放器外壳,它有一套严谨的状态机:readyloadingcanplayplayingendedpaused,每个状态都有对应的事件(loadstart,canplay,play,ended,pause)。很多 DIY 方案失败,是因为把视频切换当成“替换 src 属性”这么简单的事——但 Video.js 内部会缓存上一个视频的播放位置、音量、字幕轨道等状态,直接换 src 会导致currentTime重置、volume回退、textTracks丢失。

本插件采用官方推荐的插件模式,核心逻辑封装在videojs.registerPlugin('list', function(options) {...})中。这意味着:
- 插件实例与 Video.js player 实例共生,共享同一套事件总线;
- 可以在player.ready()后安全操作,避免 DOM 未就绪就调用player.src()
- 能监听player.on('ended', ...),但更关键的是,它能主动触发player.trigger('listnext')这样的自定义事件,让外部逻辑(比如更新列表高亮样式)也能订阅;
- 所有状态(当前播放索引、是否启用自动续播、列表数据源)都托管在player.listData = {...}这个私有属性里,不污染全局作用域。

这种设计,让“自动续播”不再是setTimeout(() => player.src(nextSrc), 100)这种脆弱的轮询,而是变成一个可预测、可调试、可拦截的事件流。比如你想在自动切到下一个视频前弹出提示:“即将播放第 3 讲,是否继续?”,只需player.off('listnext').on('listnext', (e) => { if (!confirm('继续播放?')) e.preventDefault(); })——这是框架组件很难提供的灵活性。

2.3 目录结构的设计哲学:为未来扩展留白,而非堆砌功能

看资源包目录:images/statics/lang/videojs_list/,表面是文件夹划分,实则是三层抽象:
-images/:纯资源层。存放所有封面图、占位符、加载图标。命名规则强制为video_{id}.jpg(如video_001.jpg),与视频元数据中的id字段一一对应,避免路径拼接错误;
-statics/:静态资产层。包含icons/(SVG 图标雪碧图)、fonts/(自定义字体,用于多语言标题)、config/(可选的 JSON 配置模板);
-lang/:国际化层。预留zh-CN.jsonen-US.json等文件,内容仅为{ "next": "下一个", "play": "播放", "loading": "加载中..." }这样的键值对,插件初始化时自动读取navigator.language匹配,无需修改 JS 逻辑;
-videojs_list/:核心逻辑层。index.js是主入口,list.css是样式,list.min.js是压缩版,README.md是给开发者看的 API 文档。

这种结构不追求“大而全”,而是“小而准”。比如没有内置“收藏”“分享”按钮,因为那是业务逻辑,应由宿主页面通过player.on('listitemclick', (e) => { trackEvent('favorite', e.video.id); })来实现;也没有做“分页懒加载”,因为列表项超过 50 个才需要,而这时你应该用后端接口分页,前端只负责渲染当前页的 10 个。留白,是为了让你在videojs_list/里加一行import './plugins/share-button.js';就能扩展,而不是被迫 fork 整个仓库。

3. 核心细节解析:从点击到播放,中间发生了什么?

3.1 列表渲染:为什么用<ul>而不是<div>?语义化与可访问性的硬约束

插件默认的列表模板长这样:

<ul class="vjs-list" role="tablist" aria-label="视频列表"> <li class="vjs-list-item" role="tab" tabindex="0">player.src({ src: video.src, type: 'video/mp4' }); player.preload('metadata'); // 仅加载时长、尺寸等,不下载视频流

这步耗时极短(通常 < 200ms),且 iOS 允许。此时player.duration()已可获取,列表项右侧的时长标签能立刻更新。

阶段二:缓冲关键帧(Buffer Keyframe)
监听player.one('loadedmetadata', () => { ... }),触发后调用:

player.tech().el().preload = 'auto'; // 切换为自动预加载 player.load(); // 主动触发加载

tech().el()是获取底层<video>原生元素的方法。这里我们绕过 Video.js 的封装,直接操作原生属性,因为player.load()在某些版本中存在兼容性问题。

阶段三:手势触发播放(Gesture-triggered Play)
最关键一步:player.play()必须在 click 事件回调的同步上下文中执行。插件代码里是这样写的:

$listItem.on('click touchend', function(e) { e.preventDefault(); const index = $(this).data('index'); // ... 设置当前索引、更新高亮 ... player.src(videoList[index].src); player.play().catch(err => { console.warn('自动播放被阻止,等待用户手势', err); // 此时显示一个大大的“点击播放”按钮浮层 }); });

注意player.play().catch()的处理——如果被拦截(iOS 最常见),插件会自动在视频区域中央显示一个半透明黑色遮罩层,上面居中一个白色播放图标和文字“点击开始播放”。用户点击遮罩,player.play()才真正执行。这个“降级方案”比直接报错友好得多。

3.3 自动续播:不只是监听ended,而是构建一个播放队列

很多方案的“自动续播”逻辑是:

player.on('ended', () => { const nextIndex = currentIndex + 1; if (nextIndex < videoList.length) { player.src(videoList[nextIndex].src); player.play(); } });

这在理想网络下可行,但现实中会出问题:
- 如果下一个视频很大,player.src()后立即player.play(),大概率触发NotAllowedError(iOS)或黑屏(Chrome);
- 如果用户手动拖拽进度条到结尾,ended事件不会触发,但用户期望“播完就切”;
- 如果列表只有 1 个视频,nextIndex越界,代码崩溃。

本插件的解决方案是:把播放过程抽象为一个队列(Queue)。核心数据结构是:

player.listData = { queue: [...videoList], // 原始列表 currentIndex: 0, isAutoNext: true, // 是否启用自动续播(可关闭) nextLoadPromise: null // 缓存下一个视频的加载 Promise };

自动续播逻辑封装在player.loadNextVideo()方法里:

player.loadNextVideo = function() { const nextIndex = this.listData.currentIndex + 1; if (nextIndex >= this.listData.queue.length || !this.listData.isAutoNext) { return Promise.resolve(false); // 不续播 } const nextVideo = this.listData.queue[nextIndex]; // 创建一个 Promise,resolve 时机是视频缓冲就绪 this.listData.nextLoadPromise = new Promise((resolve, reject) => { this.src(nextVideo.src); this.one('canplaythrough', () => resolve(true)); // canplaythrough 表示足够缓冲播放 this.one('error', reject); this.load(); // 主动加载 }); return this.listData.nextLoadPromise; };

然后,在ended事件里:

player.on('ended', () => { player.loadNextVideo().then(success => { if (success) { player.listData.currentIndex++; player.play(); // 此时已确保 canplaythrough,play 必然成功 player.trigger('listnext'); // 触发自定义事件 } }); });

这个设计的好处是:
-canplaythroughcanplay更可靠,它表示视频已缓冲到可连续播放的程度,不是刚解码出第一帧;
-nextLoadPromise缓存了加载状态,避免重复调用load()导致资源浪费;
-player.listData.currentIndex是唯一可信的索引源,列表 DOM 的data-index只用于初始映射,后续完全不依赖。

4. 实操过程详解:从零搭建一个可用的视频列表页

4.1 环境准备:四份文件,三分钟完成基础集成

假设你有一个空的 HTML 页面,想快速接入视频列表。不需要 Node.js,不需要 npm,只需要四个文件:

文件来源说明
video.min.jsVideo.js 官网下载 v7.20.3核心播放器,必须 7.x 版本(8.x 有 breaking change)
video-js.css同上官方默认样式,必须引入,否则控件不显示
jquery.min.jsjQuery 官网下载 v3.6.0用于 DOM 操作兼容,小于 3KB
videojs-list.min.js本资源包videojs_list/目录下插件主逻辑

在 HTML 中按顺序引入(顺序不能错!):

<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>我的视频列表</title> <!-- 1. Video.js 样式必须最先 --> <link href="statics/video-js.css" rel="stylesheet"> <!-- 2. jQuery 必须在 Video.js 之前 --> <script src="statics/jquery.min.js"></script> <!-- 3. Video.js 核心 --> <script src="statics/video.min.js"></script> <!-- 4. 本插件 --> <script src="videojs_list/videojs-list.min.js"></script> </head> <body> <!-- Video.js 播放器容器 --> <video id="my-player" class="video-js vjs-default-skin" controls preload="auto"> <source src="" type="video/mp4"> </video> <!-- 视频列表容器 --> <div id="video-list-container"></div> <script> // 初始化 Video.js player const player = videojs('my-player', { fluid: true, aspectRatio: '16:9', controlBar: { children: ['playToggle', 'volumePanel', 'currentTimeDisplay', 'progressControl', 'durationDisplay', 'fullscreenToggle'] } }); // 初始化视频列表插件 player.list({ container: '#video-list-container', // 列表渲染到哪个 DOM videos: [ { id: '001', title: '第一讲:Vue 基础入门', description: '掌握响应式原理与指令语法', src: 'videos/vue-intro.mp4', poster: 'images/video_001.jpg', duration: '12:34' }, { id: '002', title: '第二讲:组件通信', description: 'Props/Emit、Provide/Inject、Event Bus', src: 'videos/vue-component.mp4', poster: 'images/video_002.jpg', duration: '18:21' } ], autoNext: true, // 是否启用自动续播 showDescription: true, // 是否显示描述文字 language: 'zh-CN' // 多语言标识 }); </script> </body> </html>

关键点说明:
-container: '#video-list-container'指定了列表渲染位置,你可以把它放在<header>里做顶部导航,或<aside>里做侧边栏,完全自由;
-videos数组里的每个对象,字段名必须严格匹配(id,title,src,poster,duration),插件内部不做容错转换,这是为了性能——少一次Object.keys().map()遍历;
-autoNext: true是默认值,设为false即可关闭自动续播,适合“单集观看”场景;
-language字段会自动加载lang/zh-CN.json,如果文件不存在,则回退到内置英文。

4.2 自定义样式:如何修改缩略图圆角、列表间距、高亮颜色?

插件的 CSS 是模块化的,所有类名都带vjs-前缀,避免与宿主页面样式冲突。核心样式文件videojs_list/list.css结构如下:

/* 1. 列表容器 */ .vjs-list { display: flex; flex-wrap: wrap; gap: 16px; /* 列表项间距,全局控制 */ padding: 12px 0; } /* 2. 列表项 */ .vjs-list-item { display: flex; align-items: center; padding: 8px; border-radius: 8px; /* 缩略图圆角统一在此设置 */ cursor: pointer; transition: all 0.2s ease; } .vjs-list-item:hover, .vjs-list-item[aria-selected="true"] { background-color: #f0f8ff; /* 高亮背景色 */ box-shadow: 0 2px 8px rgba(0,0,0,0.1); } /* 3. 缩略图 */ .vjs-list-thumb { width: 80px; height: 45px; object-fit: cover; border-radius: 4px; /* 缩略图自身圆角 */ margin-right: 12px; } /* 4. 文字信息区 */ .vjs-list-info { flex: 1; min-width: 0; } .vjs-list-title { font-size: 14px; font-weight: 600; color: #333; margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .vjs-list-desc { font-size: 12px; color: #666; margin: 4px 0 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .vjs-list-duration { font-size: 12px; color: #999; }

修改方法极其简单:
- 打开videojs_list/list.css
- 找到对应的选择器(如.vjs-list-item);
- 修改border-radius(圆角)、gap(间距)、background-color(高亮色)等属性;
- 保存后刷新页面,实时生效。

我建议不要直接改list.min.css(压缩版),而是改list.css,然后用在线工具(如 https://www.toptal.com/developers/cssminifier)重新压缩。这样你始终保留可读的源码,便于后续维护。

4.3 多语言支持:添加西班牙语只需三步

资源包里的lang/目录是为国际化预留的。添加西班牙语(es-ES)支持,只需三步:

第一步:创建语言文件
lang/目录下新建es-ES.json,内容如下:

{ "next": "Siguiente", "play": "Reproducir", "loading": "Cargando...", "noVideo": "No hay videos disponibles", "error": "Error al cargar el video" }

第二步:在初始化时指定语言

player.list({ container: '#video-list-container', videos: [...], language: 'es-ES' // 注意大小写,必须与文件名一致 });

第三步:插件自动生效
插件内部逻辑会:
1. 检查lang/es-ES.json是否存在;
2. 存在则用fetch()加载;
3. 加载成功后,将window.videojsListLang = {...}挂载到全局;
4. 所有文案(如加载提示、错误提示)都通过window.videojsListLang.loading || 'Loading...'获取。

如果加载失败(比如网络问题),会自动 fallback 到内置英文。整个过程对使用者完全透明,你只需管好 JSON 文件的内容。

5. 常见问题与排查技巧实录:那些文档里不会写的坑

5.1 问题速查表:高频故障与一键修复

现象可能原因解决方案验证方式
点击列表项,播放器没反应,控制台报TypeError: Cannot read property 'src' of undefinedvideos数组为空,或src字段缺失检查player.list({ videos: [...] })中的数组长度,确认每个对象都有src字段console.log(player.listData.queue)查看实际加载的数据
iOS 上点击后黑屏,控制台报NotAllowedError: play() can only be initiated by a user gestureplayer.play()未在用户手势回调中同步执行确认videojs-list.min.js是最新版(v1.3.0+),旧版有异步延迟 bug更新插件,或临时在player.list()后加player.play().catch(...)测试
自动续播时,下一个视频加载缓慢,出现明显卡顿视频文件未开启 HTTP/2,或 CDN 未配置缓存将视频文件托管到支持 HTTP/2 的 CDN(如 Cloudflare),并设置Cache-Control: public, max-age=31536000用 Chrome DevTools 的 Network 面板查看waterfall,确认TTFB< 100ms
列表项点击后,高亮样式没变,还是第一个项被选中data-index属性未正确写入 DOM,或 jQuery 未加载成功检查<script>引入顺序,确保jquery.min.jsvideojs-list.min.js之前;检查列表项 HTML 是否有data-index="0"在浏览器控制台执行$('.vjs-list-item').data('index'),看是否返回数字
多语言切换后,部分文案仍是英文lang/zh-CN.json文件编码不是 UTF-8,或包含 BOM 头用 VS Code 打开 JSON 文件,右下角确认编码为UTF-8,点击“重新以编码打开” → 选择UTF-8文件另存为时,勾选“UTF-8 with BOM”选项(某些老系统需要)

5.2 实操心得:五个我踩过的坑,帮你省下三天调试时间

坑一:Poster 图片尺寸不一致,导致列表项高度参差不齐
现象:有的缩略图高 45px,有的高 60px,整个列表像锯齿一样。
原因:<img>标签的height是固定值,但poster图片宽高比不同(16:9 vs 4:3),object-fit: cover会裁剪,但容器高度仍按原始比例撑开。
解决:在 CSS 中强制统一容器高度:

.vjs-list-thumb { width: 80px; height: 45px; object-fit: cover; flex-shrink: 0; /* 防止被 flex 压缩 */ }

并在 HTML 中给<img>添加height="45"属性,双重保险。

坑二:视频地址带查询参数,导致src匹配失效
现象:src: "video.mp4?t=123456789",插件内部用===比较字符串,结果认为是新视频,重复加载。
解决:在传入videos数组前,预处理src字段,移除无意义参数:

const cleanSrc = (url) => { try { const u = new URL(url); u.search = ''; // 清空所有 query 参数 return u.toString(); } catch (e) { return url; } }; videos.forEach(v => v.src = cleanSrc(v.src));

坑三:移动端双击缩放,误触列表项
现象:iOS Safari 上,用户双击视频区域想放大,结果触发了列表项的click事件,跳转到其他视频。
解决:给列表容器添加 CSS 禁用双击缩放:

.vjs-list { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; touch-action: manipulation; /* 关键:允许点击,禁止缩放 */ }

坑四:视频加载失败后,列表项仍显示“正在播放”高亮
现象:网络中断,视频加载报错,但列表项的aria-selected="true"没被清除,用户以为还在播。
解决:监听player.error事件,主动重置状态:

player.on('error', () => { player.listData.currentIndex = -1; // 重置索引 $('.vjs-list-item').attr('aria-selected', 'false'); // 显示错误提示 });

这段代码已内置在插件中,但如果你覆盖了player.error事件,记得调用player.listData.reset()

坑五:SEO 友好性被忽略,搜索引擎抓不到视频标题
现象:百度搜索“Vue 基础入门”,你的页面没出现在结果里。
解决:在<head>中添加结构化数据(Schema.org):

<script type="application/ld+json"> { "@context": "https://schema.org", "@type": "VideoGallery", "name": "我的视频教程", "description": "Vue.js 全系列免费教程", "video": [ { "@type": "VideoObject", "name": "第一讲:Vue 基础入门", "description": "掌握响应式原理与指令语法", "contentUrl": "videos/vue-intro.mp4", "thumbnailUrl": "images/video_001.jpg", "duration": "PT12M34S" } ] } </script>

每增加一个视频,就在video数组里追加一个对象。Google Search Console 可验证效果。

6. 进阶扩展:如何为这个插件添加“播放进度同步”与“离线缓存”

6.1 播放进度同步:让用户在任意设备上接着看

“自动续播”解决的是“播完切下一个”,但用户更常问的是:“我在手机上看到第 8 分钟,回家用电脑打开,怎么继续?” 这需要播放进度同步。本插件预留了player.on('timeupdate', ...)事件钩子,你可以轻松接入。

方案一:LocalStorage 本地同步(适合单设备)

player.on('timeupdate', _.throttle(function() { const currentTime = player.currentTime(); const videoId = player.listData.queue[player.listData.currentIndex]?.id; if (videoId) { localStorage.setItem(`video_progress_${videoId}`, currentTime.toString()); } }, 10000)); // 每 10 秒存一次,减少 I/O // 初始化时恢复进度 const savedTime = localStorage.getItem(`video_progress_${currentVideo.id}`); if (savedTime && parseFloat(savedTime) > 0) { player.currentTime(parseFloat(savedTime)); }

方案二:后端 API 同步(适合多设备)
player.on('ended', ...)后,上报播放完成事件:

player.on('ended', () => { fetch('/api/video/complete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId: getCookie('userId'), videoId: player.listData.queue[player.listData.currentIndex].id, duration: player.duration() }) }); });

后端记录videoId + userId的完成状态,下次加载时,根据completed: true隐藏“未观看”标签。

6.2 离线缓存:PWA 方案让视频在地铁里也能播

视频文件太大,不可能全量缓存,但我们可以缓存“封面图 + 元数据 + 播放器框架”,让用户即使断网,也能看到列表、点击播放(如果视频已缓存过)。

Step 1:注册 Service Worker
index.html底部添加:

<script> if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js') .then(reg => console.log('SW registered!', reg)) .catch(err => console.error('SW registration failed', err)); }); } </script>

Step 2:编写sw.js

const CACHE_NAME = 'videojs-list-v1'; const urlsToCache = [ '/', '/statics/video-js.css', '/statics/video.min.js', '/videojs_list/videojs-list.min.js', '/images/video_001.jpg', '/images/video_002.jpg' ]; self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME) .then(cache => cache.addAll(urlsToCache)) ); }); self.addEventListener('fetch', event => { event.respondWith( caches.match(event.request) .then(response => response || fetch(event.request)) ); });

Step 3:视频文件缓存策略
在用户第一次播放某个视频时,主动缓存:

player.on('play', () => { const currentSrc = player.currentSrc(); if (currentSrc && !caches.has(CACHE_NAME)) { caches.open(CACHE_NAME).then(cache => { cache.add(currentSrc); // 触发缓存 }); } });

这样,用户第二次打开页面,即使断网,封面图、列表、播放器都能加载,已播放过的视频也能直接播放。

7. 总结与个人体会:一个插件的价值,不在于它多复杂,而在于它多可靠

写完这篇长文,我翻出最早一版的videojs-list.js(2021 年 10 月 30 日,也就是资源包里截图的日期),只有 187 行,功能简陋:能点、能播、能切,但没错误处理、没 iOS 兼容、没多语言。两年过去,它变成了现在这样:423 行核心逻辑、12 个可配置选项、7 种事件钩子、3 层目录抽象、全平台兼容测试报告。变化的不是代码行数,而是对“可靠”二字的理解。

我曾经以为,一个好插件应该功能丰富,支持弹幕、倍速、字幕、水印……后来在给一家医院做手术教学视频平台时,对方运维说:“我们服务器带宽只有 10Mbps,所有功能都要砍,只要求一点:护士点开视频,3 秒内必须出画面,不能黑屏。” 那一刻我删掉了所有炫技的代码,只留下最核心的三件事:点、播、续。本插件的autoNext默认开启,但你可以随时player.listData.isAutoNext = false关闭;它的 CSS 可以被覆盖,JS 可以被 monkey patch;它不强迫你用它的图标,不规定你视频必须 MP4 格式,甚至不假设你有后端——所有数据都支持前端传入。

所以,如果你正面临一个“必须快速上线、不能出错、没人帮你兜底”的视频列表需求,请放心用它。它可能不是最酷的,但在我经手的 17 个项目里,它是唯一一个上线后,我再也没有为它写过一行 hotfix 的插件。最后分享一个小技巧:在player.list()初始化后,加一行console.table(player.listData.queue.map(v => ({ id: v.id, title: v.title, size: (v.src.length / 1024).toFixed(1) + 'KB' }))),能立刻看到所有视频的 ID、标题和 URL 长度,排查路径错误快如闪电。这,就是经验。

本文还有配套的精品资源,点击获取

简介:一个轻量、即用型的 Video.js 视频列表组件,实现单页内多视频缩略图/标题展示与交互控制。用户点击任意列表项,页面立即加载对应视频并开始播放;当前视频结束或手动切换时,可无缝跳转至下一个视频。资源包内置完整运行环境:包含 index.html 入口页、video.min.js(Video.js 7.x 核心)、video-js.css 及压缩版样式文件、jquery.min.js(用于 DOM 操作兼容)、示例截图和纯文本说明文档。目录结构清晰划分 images(存放封面图)、statics(静态资源)、lang(预留多语言支持)、videojs_list(主逻辑代码),方便后续扩展图标、字幕、地区化配置。所有功能基于原生 JavaScript 封装,不依赖 Webpack/Vite 等构建工具,直接引入 HTML 即可运行,适配现有网站嵌入或快速搭建视频聚合页。


本文还有配套的精品资源,点击获取

http://www.jsqmd.com/news/948292/

相关文章:

  • Qwen3-32B-gs-A8W8量化模型性能评测:96%GSM8K准确率背后的秘密
  • PHP设计模式工厂模式详解
  • 【职场】你公司挂在墙上的使命愿景价值观,本质是一套人事物的操控系统
  • 5分钟快速上手Janus-Pro-1B:从零开始部署你的首个多模态AI应用
  • 3分钟掌握JetBrains IDE无限试用:开源重置工具终极指南
  • TinyLlama-1.1B-Chat-v0.1安全部署指南:保护AI对话系统的5个关键步骤
  • 避坑指南:Verilog写BMP图片时多出0D字节?详解二进制与文本模式区别
  • 2026年郑州地坪漆厂家全景横评:环保耐磨定制方案选购指南 - 优质企业观察收录
  • C#写的推箱子游戏源码,带关卡编辑器、操作回放和本地存档
  • 如何用EPubBuilder在线编辑器5分钟打造专业电子书
  • 微信小程序班级管理全套资源:含学生签到、作业提交、通知发布与后台管理源码
  • MusicFree插件终极指南:5分钟打造你的全能音乐播放器
  • 基于Python+Django的轻量化私有云盘系统:从零搭建安全可控的文件存储与共享平台
  • Gemma 4-31B编程能力实战:10个代码生成与调试示例
  • 新手避坑指南:用ArcGIS和SWAT2012做水文模拟,我在石羊河流域踩过的那些‘雷’
  • FunClip终极指南:3步掌握本地AI视频剪辑神器
  • 2026年江苏钢结构厂家:徐州门式钢结构/钢结构天桥/钢结构栈桥,钢板下料/钢板切割/预埋件钢板有实力的企业 - 品牌企业推荐师(官方)
  • 3分钟掌握微信小程序二维码生成:weapp-qrcode完全指南
  • 易语言乐玩插件实战:用《剑侠情缘》多开,手把手教你搞定多线程后台绑定(附源码)
  • 免费在线使用的去水印软件推荐|分场景梳理图片视频多类免费去水印实用工具
  • F28335毫秒级定时器驱动工程:LED闪烁、数码管倒计时、按键响应与蜂鸣反馈一体化示例
  • MATLAB小波图像拼接教学包:带GUI操作界面、多组实测图像与完整可运行代码
  • 洛雪音乐助手:三大音乐平台一键聚合,打造你的专属音乐库
  • 伺服电机力矩控制实现精确运动
  • VdhCoApp终极指南:如何在Mac OS Sonoma 14.2.1上完美安装与配置Video DownloadHelper伴侣应用
  • PHP设计模式策略与适配器实战
  • 手机靓号平台哪家正规?4项资质标准对照 - 资讯快报
  • 3分钟掌握洛雪音乐助手:跨平台音乐聚合播放的终极指南 [特殊字符]
  • 从一道CTF题看PHP Session反序列化:手把手教你复现HarekazeCTF2019的Easy Notes
  • 气井井口压力已知时快速推算井底流压的MATLAB工具集