
1. 背景与核心概念在日常前端项目开发中我们经常遇到一类看似简单、实则细节很多的页面视频花絮页、活动相册页、宣传物料展示页。它们通常由多段短视频、多张高清图、一段背景音乐或字幕组成用户在浏览时如果直接全部加载很容易出现首屏白屏时间长、内存占用高、流量浪费严重等问题。“拍摄花絮”本质上是一个富媒体内容场景。它不像普通图文详情页那样只处理文本和少量图片而是需要同时兼顾视频流加载、图片懒加载、曝光埋点、页面性能、播放器兼容等多项能力。很多前端新手在第一次接到类似需求时会把所有资源一股脑放进页面里用最原始的video标签和img标签硬写最后在测试阶段才发现移动端卡顿严重、流量超预算、播放状态混乱。本文就以搭建一个“拍摄花絮展示页面”为背景完整讲解如何从零到一实现一个可上线的前端富媒体页面。我们会先梳理这类页面的核心需求再基于 Vue 3 Vite Spring Boot 实现一套前后端分离的完整案例包含视频列表接口、图片懒加载、视频播放暂停管理、曝光埋点上报、性能优化策略以及移动端兼容适配中的常见坑点。这篇文章适合以下读者正在学习 Vue 3 前端工程化的开发者。需要实现视频/图片展示类页面的初中级前端工程师。后端想了解如何为富媒体页面设计接口的数据开发/Java 开发。遇到页面加载卡顿、流量消耗过高问题的业务开发人员。读完本文你将掌握富媒体页面的核心交互设计逻辑。Vue 3 组合式 API 在真实页面中的组织方式。视频播放状态管理、图片懒加载的完整实现。前后端接口字段设计与数据格式约定。曝光埋点、监听器清理、节流防抖等工程化细节。常见报错的排查思路和性能优化清单。先说明一个前提本文用到的所有数据和示例均为演示用途不涉及真实艺人信息重点在于技术方案本身。如果你所在业务中有真实的版权素材发布前需要额外注意素材授权、域名白名单、防盗链等问题。2. 环境准备与版本说明开始编码之前先把环境准备好。以下是本文示例所使用的开发环境和技术栈请根据你的实际项目情况调整版本。2.1 环境依赖环境或工具版本建议作用Node.js16.20.x 或 18.x前端构建运行环境npm / pnpmnpm 8 / pnpm 7依赖包管理JDKJDK 8 或 11后端接口服务运行环境Maven3.6.3 以上后端项目构建工具前端框架Vue 3.4.x页面开发框架构建工具Vite 5.x前端打包与本地开发服务器后端框架Spring Boot 2.7.x提供接口服务IDEVS Code IDEA分别开发前后端版本说明不同版本之间可能存在差异例如 Vue 3.2 与 3.4 在使用defineModel、v-model行为上有变化Spring Boot 2.7 与 3.x 在javax和jakarta包名上完全不同。本文示例以 Vue 3.4 Spring Boot 2.7 为例重点演示配置与开发思路如果你使用更新版本请对应调整导入语句和依赖坐标。2.2 示例项目结构为了方便阅读下面给出本文完整示例的项目结构。你可以先有个整体印象后面在实战章节会逐个文件讲解。shooting-flower/ ├── frontend/ # 前端项目 │ ├── index.html │ ├── package.json │ ├── vite.config.js │ └── src/ │ ├── main.js │ ├── App.vue │ ├── api/ │ │ └── flower.js │ ├── components/ │ │ ├── VideoCard.vue │ │ ├── LazyImage.vue │ │ └── ExposureTracker.js │ ├── hooks/ │ │ └── useIntersectionObserver.js │ ├── views/ │ │ └── FlowerDetail.vue │ └── utils/ │ └── report.js └── backend/ # 后端项目 └── src/main/java/com/example/flower/ ├── FlowerApplication.java ├── controller/ │ └── FlowerController.java ├── entity/ │ └── FlowerVideo.java └── service/ └── FlowerService.java后续代码都会按照这个结构落到具体文件路径中建议同步建立相同结构方便对照学习。3. 核心功能设计与知识点拆解在动手写代码前先把“花絮展示页面”涉及的核心功能拆解清楚。很多时候项目开发慢、返工多就是因为这一步没做好。3.1 需求清单一个典型的“拍摄花絮”页面往往包含以下需求顶部展示标题、封面大图、简介文案。中部是视频列表每个视频有封面、时长、标题。点击视频后播放播放时其他视频自动暂停。图片和视频均采用懒加载只有滚动到视口附近才加载。用户浏览到某个视频卡片时记录一次曝光用于后续数据统计。移动端适配横竖屏切换后播放状态不丢失。弱网条件下显示加载状态和错误重试。对照待开发的需求可以整理成以下功能模块功能技术方案优先级视频列表展示后端接口 前端列表渲染必须视频播放/暂停HTML5 video 标签 Vue 响应式状态管理必须图片/视频懒加载IntersectionObserver API必须自动暂停其他视频统一播放状态管理必须曝光埋点上报曝光逻辑 防抖上报建议弱网加载错误处理video error 事件 重试机制建议3.2 为什么选择 IntersectionObserver 实现懒加载实现懒加载的方式有很多比较传统的是监听页面scroll事件然后通过getBoundingClientRect()判断元素是否进入视口。但这种方式存在两个明显问题scroll 事件触发频率很高即使加了节流依然会产生大量计算。每个需要懒加载的元素都要单独计算位置逻辑分散且容易出错。IntersectionObserver是浏览器原生提供的观察接口可以异步观察目标元素与根元素的交叉状态。它由浏览器底层实现性能远高于手动监听 scroll。当元素进入视口时回调函数触发我们可以在回调里将真实的资源地址赋值给src属性从而真正加载内容。// 基础用法示意 const observer new IntersectionObserver((entries) { entries.forEach((entry) { if (entry.isIntersecting) { // 元素进入视口 const element entry.target; element.src element.dataset.src; observer.unobserve(element); } }); });使用该 API 时建议在组件卸载时调用observer.disconnect()释放观察器资源避免内存泄漏。3.3 视频播放管理的核心思路一个页面中有多个视频时最忌讳的是每个视频各自管理自己的播放状态。你可能会遇到这样的场景点击视频 A 播放再点击视频 B 播放结果 A 和 B 同时在响。这是因为每个视频组件的播放状态是独立的。正确做法是将“当前正在播放的视频 ID”提升到父组件中统一管理。子组件播放时通过事件告诉父组件“我现在开始播放了”父组件把其他子组件的播放状态改为暂停。script setup // 父组件中统一管理播放状态 const currentPlayId ref(null); function handlePlay(id) { currentPlayId.value id; } function handlePause(id) { if (currentPlayId.value id) { currentPlayId.value null; } } /script子组件通过props接收currentPlayId并在watch中判断自己是否应该暂停。这样既避免了多视频同时播放也方便后续扩展自动播放策略。3.4 曝光埋点的实现思路曝光埋点通常用于统计“用户实际看到了哪些内容”和点击埋点是两个不同的维度。在信息流场景中只有真正进入视口并停留一定时间的内容才算有效曝光。实现上可以在 IntersectionObserver 回调中判断元素进入视口然后通过防抖函数延迟上报。上报的数据一般包含内容 ID。曝光时间戳。页面来源。用户设备等信息。为了不阻塞主流程上报请求可以使用navigator.sendBeacon也可以使用图片new Image().src的方式避免影响页面性能。生产环境中通常会将上报数据聚合后批量发送这里示例只做基础展示。4. 完整实战案例章节 3 的概念理解后我们进入完整的代码实现。因为涉及前后端两个项目这里按前端和后端分部分讲解。你可以先完整实现后端接口也可以先启动前端并用 mock 数据调试完全取决于你当前的开发习惯。下面我们按照“先前端页面 → 再后端接口 → 最后联调”的顺序推进。4.1 前端项目初始化先创建前端项目。使用 Vite 初始化 Vue 3 项目npm create vitelatest frontend -- --template vue cd frontend npm install安装完成后在package.json中确认 Vue 和 Vite 版本然后创建一个干净的目录结构。4.2 安装所需依赖当前项目不需要额外的大型 UI 库核心依赖只有vue和axios。安装 axios 用于请求后端接口npm install axios如果希望移动端适配更简单可以安装postcss-px-to-viewport或使用 rem 方案。本文为了聚焦核心逻辑不引入额外适配插件实际项目按团队规范处理即可。4.3 配置 Vite 开发代理前后端分离开发时最常遇到的问题就是跨域。这里我们通过 Vite 的 proxy 配置将/api开头的请求转发到后端服务。文件路径frontend/vite.config.jsimport { defineConfig } from vite; import vue from vitejs/plugin-vue; export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } });配置完成后前端页面里请求/api/flower/videos时Vite 开发服务器会自动转发到http://localhost:8080/api/flower/videos从而避免开发环境的跨域问题。生产环境则由 Nginx 或网关统一转发。4.4 定义前端接口请求层文件路径frontend/src/api/flower.jsimport axios from axios; const request axios.create({ baseURL: /api, timeout: 10000 }); export function getFlowerVideos() { return request.get(/flower/videos); } export function reportExposure(data) { return request.post(/flower/exposure, data); }这里把接口层单独拆出来好处是页面组件不需要关心请求地址和参数组装统一封装后也方便做拦截器、错误处理等。4.5 创建图片懒加载组件图片和视频懒加载是页面性能优化的关键。首先实现一个通用的LazyImage组件负责处理普通图片的懒加载。文件路径frontend/src/components/LazyImage.vuetemplate div classlazy-image :class{ loaded: isLoaded } img v-ifisLoaded :srcsrc :altalt classlazy-image__img loadhandleLoad / div v-else classlazy-image__placeholder span加载中.../span /div /div /template script setup import { ref } from vue; import useIntersectionObserver from ../hooks/useIntersectionObserver; const props defineProps({ src: { type: String, required: true }, alt: { type: String, default: } }); const isLoaded ref(false); const placeholderRef ref(null); // 当占位元素进入视口时开始加载真实图片 useIntersectionObserver(placeholderRef, () { isLoaded.value true; }); /script style scoped .lazy-image { width: 100%; min-height: 200px; background-color: #f5f5f5; } .lazy-image__placeholder { display: flex; align-items: center; justify-content: center; height: 200px; color: #999; } .lazy-image__img { width: 100%; display: block; } /style核心逻辑在useIntersectionObserver中这是一个组合式函数可以在多个组件中复用。文件路径frontend/src/hooks/useIntersectionObserver.jsimport { onMounted, onBeforeUnmount } from vue; export default function useIntersectionObserver(targetRef, callback) { let observer null; onMounted(() { const target targetRef.value; if (!target) return; observer new IntersectionObserver((entries) { entries.forEach((entry) { if (entry.isIntersecting) { callback(); if (observer) { observer.unobserve(target); } } }); }, { rootMargin: 0px 0px 200px 0px }); observer.observe(target); }); onBeforeUnmount(() { if (observer) { observer.disconnect(); observer null; } }); }rootMargin设置为200px的意思是当目标元素距离视口底部还有 200px 时就提前触发加载这样用户滚动到图片位置时图片已经加载完成或正在加载中视觉上更流畅。4.6 创建视频卡片组件视频卡片是页面的核心部分。它需要处理视频的播放、暂停、加载状态、错误重试同时也要上报曝光。文件路径frontend/src/components/VideoCard.vuetemplate div classvideo-card refvideoCardRef div classvideo-card__cover v-if!isPlaying LazyImage :srccoverUrl :alttitle / div classvideo-card__play-btn clickhandlePlay span classicon-play▶/span /div /div video v-else classvideo-card__video :srcvideoUrl refvideoRef controls autoplay playsinline webkit-playsinline pausehandlePause endedhandleEnded errorhandleError /video div classvideo-card__info h3{{ title }}/h3 span classvideo-card__duration{{ durationText }}/span /div div classvideo-card__status v-ifloadFailed span视频加载失败/span button clickretryLoad重试/button /div /div /template script setup import { ref, computed, watch, onMounted, onBeforeUnmount } from vue; import LazyImage from ./LazyImage.vue; import useIntersectionObserver from ../hooks/useIntersectionObserver; import { reportExposure } from ../api/flower; const props defineProps({ video: { type: Object, required: true }, currentPlayId: { type: [String, Number], default: null } }); const emit defineEmits([play, pause]); const isPlaying ref(false); const loadFailed ref(false); const hasReportedExposure ref(false); const videoRef ref(null); const videoCardRef ref(null); const durationText computed(() { const seconds props.video.duration || 0; const min Math.floor(seconds / 60); const sec seconds % 60; return ${String(min).padStart(2, 0)}:${String(sec).padStart(2, 0)}; }); const coverUrl computed(() props.video.coverUrl); const videoUrl computed(() props.video.videoUrl); // 曝光上报 useIntersectionObserver(videoCardRef, () { if (!hasReportedExposure.value) { hasReportedExposure.value true; reportExposure({ videoId: props.video.id, exposeTime: Date.now(), type: flower_video }); } }); // 统一控制播放/暂停 watch(() props.currentPlayId, (newId) { if (newId ! props.video.id isPlaying.value) { pauseVideo(); } }); function handlePlay() { isPlaying.value true; loadFailed.value false; emit(play, props.video.id); } function handlePause() { emit(pause, props.video.id); } function handleEnded() { isPlaying.value false; emit(pause, props.video.id); } function handleError() { loadFailed.value true; isPlaying.value false; } function pauseVideo() { isPlaying.value false; if (videoRef.value) { videoRef.value.pause(); } } function retryLoad() { loadFailed.value false; isPlaying.value true; emit(play, props.video.id); } onBeforeUnmount(() { pauseVideo(); }); /script style scoped .video-card { background: #fff; border-radius: 8px; overflow: hidden; margin-bottom: 16px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); } .video-card__cover { position: relative; } .video-card__play-btn { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 56px; height: 56px; border-radius: 50%; background: rgba(0, 0, 0, 0.6); display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 22px; } .video-card__info { padding: 12px 16px; display: flex; justify-content: space-between; align-items: center; } .video-card__status { padding: 12px 16px; text-align: center; background: #fff7e6; } .video-card__video { width: 100%; max-height: 400px; display: block; } /style4.7 创建视频花絮页面将视频卡片组件组装到页面中并统一管理播放状态。文件路径frontend/src/views/FlowerDetail.vuetemplate div classflower-detail !-- 页面头部信息 -- header classflower-detail__header h1{{ pageInfo.title }}/h1 p{{ pageInfo.desc }}/p /header !-- 视频列表 -- main classflower-detail__list VideoCard v-forvideo in videoList :keyvideo.id :videovideo :current-play-idcurrentPlayId playcurrentPlayId $event pausehandlePause / /main !-- 加载状态 -- div v-ifloading classflower-detail__loading加载中.../div div v-iferror classflower-detail__error p{{ error }}/p button clickfetchData重新加载/button /div /div /template script setup import { ref, onMounted } from vue; import VideoCard from ../components/VideoCard.vue; import { getFlowerVideos } from ../api/flower; const pageInfo ref({ title: , desc: }); const videoList ref([]); const loading ref(false); const error ref(); const currentPlayId ref(null); async function fetchData() { loading.value true; error.value ; try { const res await getFlowerVideos(); pageInfo.value res.data.pageInfo; videoList.value res.data.list; } catch (e) { error.value 数据加载失败请检查网络后重试; } finally { loading.value false; } } function handlePause() { currentPlayId.value null; } onMounted(() { fetchData(); }); /script style scoped .flower-detail { max-width: 640px; margin: 0 auto; padding: 16px; } .flower-detail__header h1 { font-size: 24px; margin-bottom: 8px; } .flower-detail__header p { color: #666; line-height: 1.6; margin-bottom: 24px; } .flower-detail__loading { text-align: center; padding: 40px 0; color: #999; } /style4.8 配置入口文件文件路径frontend/src/main.jsimport { createApp } from vue; import App from ./App.vue; createApp(App).mount(#app);文件路径frontend/src/App.vuetemplate FlowerDetail / /template script setup import FlowerDetail from ./views/FlowerDetail.vue; /script4.9 后端接口实现前端页面需要的数据结构请参考以下约定{ code: 0, message: success, data: { pageInfo: { title: 紫雾娇妆拍摄花絮, desc: 记录拍摄过程中的精彩瞬间与幕后故事 }, list: [ { id: 1, title: 妆造花絮紫雾妆容诞生记, coverUrl: https://example.com/cover1.jpg, videoUrl: https://example.com/video1.mp4, duration: 45 } ] } }后端使用 Spring Boot 实现创建一个简单的数据接口。文件路径backend/src/main/java/com/example/flower/entity/FlowerVideo.javapackage com.example.flower.entity; public class FlowerVideo { private Long id; private String title; private String coverUrl; private String videoUrl; private Integer duration; public FlowerVideo(Long id, String title, String coverUrl, String videoUrl, Integer duration) { this.id id; this.title title; this.coverUrl coverUrl; this.videoUrl videoUrl; this.duration duration; } public Long getId() { return id; } public void setId(Long id) { this.id id; } public String getTitle() { return title; } public void setTitle(String title) { this.title title; } public String getCoverUrl() { return coverUrl; } public void setCoverUrl(String coverUrl) { this.coverUrl coverUrl; } public String getVideoUrl() { return videoUrl; } public void setVideoUrl(String videoUrl) { this.videoUrl videoUrl; } public Integer getDuration() { return duration; } public void setDuration(Integer duration) { this.duration duration; } }文件路径backend/src/main/java/com/example/flower/service/FlowerService.javapackage com.example.flower.service; import com.example.flower.entity.FlowerVideo; import org.springframework.stereotype.Service; import java.util.Arrays; import java.util.List; Service public class FlowerService { public ListFlowerVideo getVideoList() { return Arrays.asList( new FlowerVideo(1L, 妆造花絮紫雾妆容诞生记, https://example.com/cover1.jpg, https://example.com/video1.mp4, 45), new FlowerVideo(2L, 灯光调整与镜头走位, https://example.com/cover2.jpg, https://example.com/video2.mp4, 120), new FlowerVideo(3L, 摄影师眼中的紫雾瞬间, https://example.com/cover3.jpg, https://example.com/video3.mp4, 80) ); } }文件路径backend/src/main/java/com/example/flower/controller/FlowerController.javapackage com.example.flower.controller; import com.example.flower.entity.FlowerVideo; import com.example.flower.service.FlowerService; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.List; import java.util.Map; RestController RequestMapping(/api/flower) public class FlowerController { private final FlowerService flowerService; public FlowerController(FlowerService flowerService) { this.flowerService flowerService; } GetMapping(/videos) public MapString, Object getVideos() { ListFlowerVideo list flowerService.getVideoList(); MapString, Object pageInfo new HashMap(); pageInfo.put(title, 紫雾娇妆拍摄花絮); pageInfo.put(desc, 记录拍摄过程中的精彩瞬间与幕后故事); MapString, Object data new HashMap(); data.put(pageInfo, pageInfo); data.put(list, list); MapString, Object result new HashMap(); result.put(code, 0); result.put(message, success); result.put(data, data); return result; } PostMapping(/exposure) public MapString, Object reportExposure(RequestBody MapString, Object payload) { System.out.println(收到曝光上报: payload); MapString, Object result new HashMap(); result.put(code, 0); result.put(message, success); return result; } }文件路径backend/src/main/java/com/example/flower/FlowerApplication.javapackage com.example.flower; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication public class FlowerApplication { public static void main(String[] args) { SpringApplication.run(FlowerApplication.class, args); } }4.10 运行与验证启动后端cd backend mvn spring-boot:run启动前端cd frontend npm run dev浏览器访问http://localhost:5173你应该看到页面顶部显示了“紫雾娇妆拍摄花絮”标题和描述。视频列表正常渲染。图片滚动到视口附近才加载。点击视频的播放按钮后视频开始播放其他视频自动停止。浏览器控制台 Network 面板可以看到曝光上报请求。5. 常见问题与排查思路在实际开发中即使代码逻辑看起来正确还是会遇到各种意外。以下是这个场景下最容易踩到的问题以及排查思路。问题现象常见原因解决思路图片懒加载不触发图片一直显示“加载中”目标元素的祖先元素有overflow: hidden或高度异常导致 IntersectionObserver 计算交叉状态错误检查滚动容器必要时在 observer 中指定root参数为最近的滚动父容器视频点击后无法播放控制台报跨域错误视频资源地址和前端域名不是同源且视频服务器未配置 CORS 头在视频服务器或 CDN 上增加跨域响应头开发环境使用 Vite proxy 转发多个视频同时播放子组件没有响应父组件的currentPlayId变化确认在watch中正确处理暂停逻辑且播放时 emit 了正确的视频 IDAndroid 上视频无法自动播放浏览器自动播放策略限制必须要求用户手势触发播放确保点击播放按钮后设置video.src并调用play()不要在异步回调中触发放置首屏加载慢页面白屏时间长视频和图片资源没有懒加载或者 JS 打包体积过大对视频、图片使用懒加载并检查打包产物中是否有不必要的依赖视频播放结束后列表仍然高亮显示播放状态ended事件中只设置了isPlayingfalse但父组件的currentPlayId未重置在handleEnded中 emitpause事件让父组件同步清空播放 ID曝光数据重复上报每次进入视口都会触发回调没有做一次性标记加hasReportedExposure标志位上报后observer.unobserve页面上有大量图片时内存占用高图片未做尺寸压缩直接使用原图服务端或 CDN 提供按需裁剪的图片地址并设置合适的缩略图参数滚动页面时明显卡顿scroll 监听或过度频繁的曝光上报使用 IntersectionObserver 替代 scroll 监听上报增加节流或批量聚合6. 最佳实践与工程建议6.1 前端工程化最佳实践视频列表接口建议做分页或流式加载。如果一次返回几百条视频即使用了懒加载也会导致内存和 DOM 节点过多。可以使用虚拟滚动技术优化长列表性能。图片 URL 统一走 CDN并配置参数。例如?imageMogr2/thumbnail/400x这种方式在图片裁切时非常有用。前端不要直接使用原图。视频资源建议使用 HLS 或 DASH 等流媒体格式而不是一个完整的 MP4 文件。完整 MP4 在移动端拖动进度条时往往要加载整个文件用户体验很差。组件卸载时一定要清理事件监听、IntersectionObserver、定时器等资源。在 Vue 3 组合式 API 中onBeforeUnmount是最常用的清理时机。曝光上报不要影响主流程。上报接口失败不应该阻塞页面正常交互建议在上报逻辑中使用catch捕获异常并考虑将多次曝光合并成一次批量上报。6.2 后端与接口设计建议曝光上报使用POST请求且不建议在个位毫秒级别实时上报。更合理的是前端缓存一批数据后每 3~5 秒批量上报。接口返回的数据结构建议统一封装。示例中的code message data结构虽然简单但在真实项目中可以扩展为包含traceId、serverTime等字段便于排查问题。视频和图片资源必须在后端或网关层配置防盗链否则资源地址被泄露后容易被盗用。如果视频列表包含大量字段考虑在接口中做字段筛选前端只需要展示所需字段减少传输数据量。6.3 安全与合规注意事项涉及素材类页面时安全问题不能忽视所有素材地址必须走公司资源管理系统避免外链引用无版权图片。视频、图片地址要动态生成签名 URL防止被任意抓取后盗用。曝光和用户行为数据属于用户隐私数据采集时必须明确告知用户且不应记录与业务无关的敏感字段。即使只是演示页面也不能把人名、肖像、敏感信息随意放到公开项目中。7. 总结与学习路线本文以一个“拍摄花絮展示页面”为切入点覆盖了前后端分离开发中的多个关键技术点。你学会了如何设计富媒体页面的核心功能如何用 Vue 3 组合式 API 组织组件逻辑如何通过 IntersectionObserver 实现高性能的图片和视频懒加载如何统一管理多个视频组件的播放状态以及如何实现曝光埋点上报和异常处理。如果你接下来想继续深入学习可以从这几个方向展开将视频播放组件迁移到成熟的播放器库比如 video.js 或西瓜播放器了解它们如何封装播放状态和适配移动端。研究虚拟滚动原理尝试在长列表页中只渲染可见区域的卡片组件。学习性能分析用 Lighthouse 或 Chrome Performance 面板查看页面在弱网环境下的表现。如果后端接口逐渐变复杂可以学习使用 Spring Boot 整合 Redis 缓存列表数据减少数据库查询压力。在做实际项目时优先关注第一个版本的接口字段是否稳定、资源地址是否正确、播放器和移动端是否兼容。这三个问题占了富媒体页面开发中很大一部分排查时间。最后提醒一句如果这篇文章对你有帮助可以先把示例代码跑起来再自己动手加一个新功能比如双击视频全屏播放、增加进度条记忆功能或者给视频卡片增加点赞交互。只有亲自实践才能真正把代码逻辑变成自己的东西。