早几年我折腾前后端分离项目的时候,用的最多的还是jQuery那套思路,后来Vue.js火起来,我把一个内部管理后台全部用Vue重写过一遍,那种“数据驱动视图、组件化复用”的开发体验确实让人回不去。这次要聊的项目就是我业余时间做的一个影视类Web应用——天天影视云视听平台的前端设计。整个项目基于Vue.js生态实现,走的是单页应用路线,包含了首页内容流、搜索筛选、视频播放、用户个人中心等完整闭环。
这个平台前端部分的核心关键词其实很聚焦:Vue.js全家桶、组件化、状态管理、路由权限、播放器集成。如果你正在做一个内容型Web产品,或者想练手一个体量适中、覆盖面广的Vue实战项目,这篇文章会很有参考价值。我会把这套系统的设计思路、技术选型的过程、开发过程中踩过的坑,以及最终沉淀下来的可复用方案,完整地捋一遍。
1. 先聊整体设计:为什么用Vue.js搭影视平台
1.1 这个项目到底要解决什么问题
影视类平台的产品形态,放到前端来说有很强的代表性。别管是优酷爱奇艺还是B站,界面再花哨,功能骨架都绕不开这几件事:内容展示、分类检索、播放器、用户体系。天天影视云视听平台本质上也是这么一套东西,只不过我把它做成一个前端主导的演示级产品,业务数据通过Mock接口模拟,重点打磨前端的工程化能力和交互体验。
这类项目最大的特点是页面多、组件复用率高、交互状态复杂。首页要推轮播图,榜单要展示热度排行,搜索页要支持多条件筛选,播放页要处理视频源切换,个人中心要展示观看历史和收藏列表。如果每个页面都独立写一套逻辑,代码量会失控,维护起来更是噩梦。组件化的开发方式正好能解决这个问题,Vue在这方面的表现又特别自然。
1.2 框架选型时我对比过什么
选型阶段,我实际在Vue 3和React之间摇摆过一阵子。React的生态和灵活性肯定没问题,但对这类内容展示型应用来说,Vue的模板语法和响应式机制写起来更直接。尤其是列表渲染、条件渲染这类高频场景,Vue的v-for和v-if在模板里一行代码就能解决,React这边虽然也能写,但心智负担明显更大。
再考虑到国内社区的积累,Vue的中文文档质量、组件库生态、踩坑资料都比React要丰富,对中小型团队和个人开发者非常友好。加上Vue 3的Composition API把逻辑复用能力补上了,大型项目的代码组织也不会有压力,选Vue.js几乎是顺理成章的决定。
1.3 技术栈全貌和版本选型
说到具体的技术栈,我是按下面这套组合来搭的,都是目前Vue生态里比较成熟的方案:
| 模块 | 选型 | 主要考虑 |
|---|---|---|
| 构建工具 | Vite | 开发启动快,HMR体验好 |
| 前端框架 | Vue 3.4(Composition API) | 组合式写法更适合模块化 |
| 路由 | Vue Router 4 | 配合SPA场景做页面切换 |
| 状态管理 | Pinia | API简洁,TS支持好 |
| HTTP库 | Axios | 请求封装成熟,拦截器方便 |
| UI组件库 | Naive UI | 样式现代,TypeScript友好 |
| 播放器 | Vue-CoreVideoPlayer | 基于video.js,功能完整 |
这里面有个细节值得注意:Vue 3配Pinia是官方推荐的组合了,Vuex现在已经不算是第一选择。Pinia的Store写法更像普通模块,模板里不需要写一串mapState,直接store.xxx用就行,代码量减少特别明显。
注意:Vue 3的生态在2024年以后已经完全成熟,Element Plus、Ant Design Vue、Naive UI这几个主流组件库都适配了Vue 3。如果是新项目,直接上Vue 3生态没问题,没必要再考虑Vue 2的存量方案。
2. 核心功能模块拆解:一个影视平台需要做哪些事
2.1 首页内容流的设计思路
首页是这类产品的门面,也是对组件化能力考验最大的页面。我把它拆成了顶部Navbar、轮播Banner、精选片单、热播榜单、最新上线这几个独立区块。每个区块都是一个子组件,数据通过props传进来,组件内部只管渲染。
这样做最大的好处是,后台上线一个新的运营位,前端只需要新写一个区块组件,然后塞到首页布局的对应位置,完全不需要动其它代码。我当时甚至把不同区块组件做成了异步加载,首页首屏只渲染轮播和精选片单,下面的区域滚动到附近再加载,实测首屏时间降了差不多30%。
榜单模块用的是v-for循环渲染,性能是关键。Vue 3的响应式系统对大型列表已经做了优化,但为了防止页面卡顿,我在长列表组件里还是加了v-memo指令,让数据没变化的时候跳过渲染。这个指令在Vue 3.2以后才提供,用起来相当顺手。
2.2 搜索和筛选体系的实现
搜索页看起来简单,实际上牵扯到路由传参、状态同步、防抖处理、筛选条件管理好几个方面。我的做法是让搜索关键字和筛选条件全部保存在URL的query参数上,这样用户刷新页面后搜索状态不会丢失,也方便分享链接给朋友。
// 搜索条件同步到URL const updateQueryParams = (params) => { const route = useRoute() const router = useRouter() const query = { ...route.query } for (const key in params) { if (params[key] !== '' && params[key] !== null) { query[key] = params[key] } else { delete query[key] } } router.replace({ path: route.path, query }) }搜索框的输入事件做了300毫秒防抖,用户停下来以后才发起请求,避免每敲一个字就触发一次接口调用。筛选条件则是联动逻辑,选了类型之后,年份和地区选项会跟着变化,这块数据我放在Pinia里统一管理,组件之间同步状态不会出现遗漏。
2.3 播放器模块的设计取舍
播放器是整个平台的核心。我研究过视频云的iframe接入和video.js原生集成两种方案,最后选了后者。Vue-CoreVideoPlayer这个组件基于video.js封装,提供了一套Vue式的API,视频源、封面、清晰度切换都能直接用props和事件来控制。
播放器的状态管理没法在Pinia里全局存,因为每个页面只需要维护自己播放器的状态,跨页面共享反而会产生冲突。所以播放器内部的状态用ref加上watch来管理,只有播放记录这个维度需要写入Pinia并同步给后端。
播放器这块有个特别需要注意的点:视频源的清晰度切换要在播放器实例创建时就预置好,而不是等用户点击再动态塞源。实测下来,动态更换视频源很容易导致播放器状态错乱,表现就是黑屏、播放进度丢失、声音还在但画面不刷新。预置多清晰度源是最稳妥的做法。
2.4 状态层的Store划分策略
Pinia的Store划分我按照业务域拆了三个:useUserStore负责用户信息和登录状态,useContentStore负责内容数据、筛选条件和榜单,usePlayerStore负责播放进度和最近播放列表。
有个经验是Store不要建太多,否则跨Store互相调用会变得非常恶心。比如用户收藏了一部影片,这个动作同时涉及用户信息和内容数据,如果分得太细,代码里全是storeA调用storeB,逻辑会绕来绕去。我的处理方式是收藏动作直接写在useContentStore里,但它内部会调用useUserStore去拿用户ID,单向依赖,思路清晰也不会出错。
3. 实操过程:从零搭建天天影视云视听平台前端
3.1 工程化搭建的具体步骤
项目初始化我没用yarn create vue模板,而是走的create-vite,再手动补齐Vue Router和Pinia。这样做的目的是更清楚每一步装了什么东西,遇到版本冲突的时候排查起来方便。
# 创建Vite项目 npm create vite@latest tianying-web -- --template vue # 进入项目目录 cd tianying-web # 安装核心依赖 npm install vue-router@4 pinia axios # 安装UI组件库 npm install naive-ui # 安装播放器组件 npm install vue-core-video-player安装完依赖以后,我做的第一件事不是写页面,而是把目录结构定好。这个非常重要,后期项目规模变大之后,一个清晰的目录结构能省掉大量找代码的时间。
src/ api/ # 接口请求封装 assets/ # 静态资源 components/ # 公共组件 layout/ # 布局组件 router/ # 路由配置 stores/ # Pinia状态 views/ # 页面组件 home/ # 首页 search/ # 搜索页 player/ # 播放页 user/ # 个人中心 utils/ # 工具函数3.2 路由设计和懒加载处理
路由层面,我采用的是组件懒加载加路由级代码分割。影视类应用页面数量多,首屏加载时要控制JS包体积,不能让用户等太久白屏。
const routes = [ { path: '/', component: () => import('@/layout/MainLayout.vue'), children: [ { path: '', name: 'Home', component: () => import('@/views/home/HomeView.vue'), meta: { title: '首页' } }, { path: 'search', name: 'Search', component: () => import('@/views/search/SearchView.vue'), meta: { title: '搜索' } }, { path: 'player/:videoId', name: 'Player', component: () => import('@/views/player/PlayerView.vue'), meta: { title: '播放' } } ] } ]路由懒加载配合Vite的构建配置,每个页面的JS文件会被单独打成chunk。实测下来,首屏只加载首页需要的代码,其它页面的JS按需加载,整体包体从最初的850KB降到了210KB左右,这个优化对体验的改善特别明显。
路由守卫那块我做了用户态的校验。个人中心和播放收藏列表这类页面,需要登录才能访问。守卫里判断用户Store里有没有token,没有就跳转到个人中心强制登录,登录完再跳回来。这种用meta标记权限位的方式,扩展起来很方便,后续加了VIP专属页面,只需要改路由的meta字段就行。
3.3 接口层封装与Mock方案
项目前中期没有真实的后端接口,我用的是Mock方案。Vite生态里有vite-plugin-mock这个插件,可以在本地开发服务器上拦截请求,返回定义好的Mock数据。它的好处是等后端接口开发好了以后,只需要把Mock插件关掉,把请求地址改成真实地址,业务层代码完全不用动。
Axios封装时我注意了几个细节。基础URL单独放在环境变量文件里,不同环境用不同的.env文件配置。请求拦截器里统一带上token,响应拦截器里统一处理错误码。业务状态码是200就返回数据,是401就清掉用户状态并跳转到登录页,其它状态码弹错误提示。
const service = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 10000 }) // 请求拦截器:附加token service.interceptors.request.use((config) => { const userStore = useUserStore() if (userStore.token) { config.headers.Authorization = `Bearer ${userStore.token}` } return config }) // 响应拦截器:统一处理业务状态 service.interceptors.response.use( (response) => { const { code, data } = response.data if (code === 200) { return data } if (code === 401) { const userStore = useUserStore() userStore.logout() router.push('/user/login') } return Promise.reject(new Error(`业务错误: ${code}`)) }, (error) => { return Promise.reject(error) } )3.4 播放器集成时踩的坑
播放器这块我折腾的时间最久。Vue-CoreVideoPlayer这个组件的基本用法很简单,在模板里放一行组件代码,传视频源和封面就行。但真正对接业务需求的时候,问题就来了。
第一个坑是播放器组件在Vue 3下默认是全屏样式,会遮挡住页面的其它内容。后来我发现需要给播放器设置一个容器高度,并且要让播放器覆盖到容器内,而不是撑满整个视口。这个需要在样式中显式声明:
.player-container { width: 100%; max-width: 960px; margin: 0 auto; } .player-container :deep(.vcvp-container) { width: 100%; height: auto; position: relative; }第二个坑是清晰度切换。影视平台必须有流畅、高清、超清这几个档位。我一开始是在用户点击的时候动态改src,结果播放器直接卡死。查了文档才知道这个组件没有提供动态切换播放源的完善支持,要在初始化的时候就配置好PlaybackRate列表,或者干脆在切换时重建播放器组件,通过v-if强制刷新实例。
3.5 性能优化三板斧
影视类平台的首页信息量巨大,图片、视频封面、榜单内容动辄几十个请求,性能优化基本上要贯穿整个开发过程。我主要做了三件事,实测效果都很直接。
第一件事是图片懒加载。用Vue的v-lazy指令配合vue-lazyload插件实现。列表中的图片统一走懒加载,只有滚动到可视区域附近才开始加载。这个对首页的加载速度影响最大,因为图片请求数量直接从几十个降到首屏的五六个。
第二件事是组件级缓存。平台上很多页面组件切走了再切回来,如果不做处理,用户每次返回都要重新请求数据。我在列表页用keep-alive包裹路由出口,并且included指定名单,只缓存列表类页面,播放页和搜索页不缓存,因为这两个页面的状态必须实时更新。
<router-view v-slot="{ Component }"> <keep-alive :include="['HomeView', 'RankView']"> <component :is="Component" /> </keep-alive> </router-view>第三件事是首屏预加载。Vite构建的SPA应用在首屏一般会有一个很短的空白时间,因为我用preload指令给首屏用到的核心组件加了预加载。路由对应的chunk在首页挂载完成之后,判断浏览器空闲了再加载其它页面模块,这样用户点进搜索页的时候瞬间就能展示,不会出现点击后等待白屏的情况。
4. 常见问题与排查技巧:把这些坑提前踩平
4.1 跨域问题排查
开发时前端跑在5173端口,Mock接口跑在3000端口,跨域是必然的。我的处理方案是在Vite配置里加代理,把/api开头的请求代理到Mock服务器,这样浏览器请求的是同源的5173端口,服务端去转发到3000端口,不会触发浏览器的跨域限制。
// vite.config.js server: { proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true } } }如果生产环境还是遇到跨域,那就得看后端能不能配CORS头。一般企业里这两种方案都会有,开发环境用代理,生产环境靠CORS。前端这边要做的就是把baseURL和环境变量分开,切换环境时只改配置,不动代码。
4.2 路由懒加载后的白屏问题
项目跑了一段时间后,有段时间频繁出现白屏,刷新就好了。排查了很久发现是路由懒加载和打包缓存共同导致的。每次发版之后,用户浏览器还缓存着旧的index.html,加载的JS文件名还是旧版本的,服务器那边文件已经更新了,找不到旧文件就报错白屏。
解决办法是双重的。第一,打包后的文件名加上hash,Vite默认就是这么做的。第二,在发布的时候对index.html设置不缓存,对带hash的静态文件设置长缓存。这样就保证了用户拿到的HTML里永远是最新版本的文件引用,而静态文件因为带hash又能充分利用浏览器缓存。这个经验在CICD团队的配合中已经验证过多次。
4.3 列表页滚动位置丢失问题
用keep-alive缓存页面组件后,出现了一个非常隐蔽的问题:列表页往下面翻了很长的距离,点进详情页再回来,页面的数据还在,但滚动条位置回到了顶部。用户如果每翻几页就点进来看一次,这个体验会非常割裂。
解决方案是记录每个页面组件的滚动位置。在列表页的onActivated生命周期里恢复上次记录的滚动偏移量,在onDeactivated里保存当前滚动偏移量。Vue 3的nextTick结合window.scrollTo(0, scrollTop)就能做到秒级恢复。这个逻辑写成Composable函数,哪个页面需要就引用一下,代码复用很方便。
4.4 视频播放页卡顿现象分析
播放器刚接入的时候,在低配设备上会有明显卡顿,画面不跟手,拖动进度条也响应慢。排查发现是播放器初始化和整个页面其它数据的加载互相抢占主线程资源,导致渲染线程一直没有空闲时间。
我的优化策略有两个方向。一是把播放器实例的创建时机往后挪,在页面基础数据已经渲染完成之后再初始化播放器,避免渲染和数据加载同时抢占资源。二是开启video.js的硬件解码相关配置,让浏览器优先使用硬件加速来处理视频解码,降低CPU负载。最终效果是低配设备上播放流畅度好了非常多。
4.5 参数对比建议速查表
把上面遇到的几个关键问题整理成了一张速查表,方便后续开发同类项目时对照:
| 问题 | 核心原因 | 推荐方案 | 优先级 |
|---|---|---|---|
| 首屏加载慢 | JS包过大、请求过多 | 路由懒加载、图片懒加载 | 高 |
| 页面切换白屏 | 路由chunk加载失败 | 打包hash、HTML不缓存 | 高 |
| 列表滚动丢失 | keep-alive重置滚动 | 记录并恢复滚动位置 | 中 |
| 播放器卡顿 | 渲染线程资源竞争 | 延迟初始化、硬件解码 | 中 |
| 跨域报错 | 浏览器同源策略 | Vite代理或CORS | 高 |
| Mock到真实接口切换 | 环境配置不统一 | 环境变量分开管理 | 低 |
5. 我沉淀下来的经验:这几个细节最值得学
整个项目从立项到跑通,再到打磨体验,我感受最深的一点是:框架本身永远不是瓶颈,对业务场景的理解才是。Vue.js把数据和视图的关联做得足够顺手,但如果不在工程化、组件划分、性能优化这些层面下功夫,写出来的项目照样可能又乱又卡。
天天影视云视听平台这个项目,价值不在说它用了多新潮的技术,而是完整覆盖了内容型Web应用的典型场景。你把它当影视平台看,它是一个能跑的产品;你把它当Vue实战教程看,它又包含了路由权限、状态管理、组件通信、接口封装、性能优化这些前端工程师天天面对的问题。我当时做完这个项目以后,再接手公司里的内容运营后台,明显感觉思路清晰了许多,遇到类似的需求能直接复用之前沉淀的方案。
最后再分享一个实操心得:组件库能就不用自己造轮子,但如果要用,先在小项目里把组件库的坑踩一遍再上生产。我刚开始用Naive UI时,有几个组件的API调用方式跟文档写的不完全一致,直接在平台上用差点出线上事故。先在Demo项目里把文档翻一遍,能少走很多弯路。这套平台的前端设计思路,其实换到任何内容型产品上都适用,你拿到手以后可以直接按着这个思路去搭自己的项目骨架。