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

SpringBoot+Vue3全栈实战:从零搭建视频点播网站

1. 项目概述与核心价值

最近有不少朋友在后台私信,想了解如何从零开始搭建一个功能完整的视频播放网站。这确实是一个挺有意思的练手项目,它几乎涵盖了现代Web开发中前后端分离架构的所有核心知识点。我自己也基于SpringBoot和Vue3,完整地走了一遍这个流程,踩了不少坑,也积累了一些心得。今天,我就把这个项目的实现思路、技术选型、关键代码以及那些“教科书上不会写”的实操细节,系统地分享出来。

这个项目本质上是一个小型的在线视频点播平台。用户可以在前端页面浏览视频列表、搜索视频、观看视频(支持进度条拖拽、清晰度切换等),而后台管理员则能上传视频、管理视频信息。听起来简单,但里面涉及的技术栈相当立体:后端需要处理视频文件上传、存储、转码、生成播放信息接口;前端要构建流畅的播放器交互;前后端之间还要考虑跨域、鉴权、大文件上传断点续传等问题。选择SpringBoot和Vue3这对组合,是因为它们分别代表了当前Java后端和JavaScript前端领域最主流、最成熟的解决方案,生态丰富,社区活跃,能让我们把精力更多地集中在业务逻辑本身,而不是框架的疑难杂症上。

2. 技术栈选型与项目架构设计

2.1 后端技术栈深度解析

后端我们选用SpringBoot 3.x。它最大的好处就是“开箱即用”,通过Starter依赖能快速集成我们需要的几乎所有组件。对于视频网站,以下几个依赖是核心:

  1. Spring Web & Spring MVC: 提供RESTful API的基础框架。
  2. Spring Data JPA / MyBatis-Plus: 用于数据持久化。我个人更倾向于MyBatis-Plus,它在MyBatis的基础上做了大量增强,单表CRUD几乎不用写SQL,但对于复杂联表查询又保留了原生MyBatis的灵活性。视频表、用户表、分类表之间的关联查询用它很合适。
  3. Spring Security + JWT: 处理用户认证与授权。这是项目的安全基石。我们采用无状态的JWT(JSON Web Token)方案,用户登录后,服务器生成一个Token返回给前端,前端后续请求都在Header中携带此Token。这样后端服务本身就不需要维护Session,更易于扩展。
  4. Spring Boot DevTools: 开发热重启,提升效率。
  5. 数据库驱动: 根据你选的数据库来,MySQL 8.x或PostgreSQL 14+都是不错的选择。
  6. 第三方库:
    • Hutool: 国产工具类库,处理日期、加密、IO等操作非常方便。
    • Lombok: 通过注解自动生成Getter/Setter、构造方法等,让实体类代码更简洁。
    • JJWT: 用于生成和解析JWT Token。

注意:在SpringBoot 3.x中,Jakarta EE 9+是默认标准,所以引入相关依赖时(如Servlet API、JPA)要注意版本,避免与旧的javax包冲突。

2.2 前端技术栈构建思路

前端我们使用Vue 3的组合式API(Composition API)和<script setup>语法,配合Vite构建工具,开发体验非常流畅。

  1. 核心框架: Vue 3。相比于Vue 2,其组合式API在逻辑复用和组织复杂组件时优势明显。
  2. 构建工具: Vite。启动速度和热更新速度远超Webpack,对于开发阶段体验提升巨大。
  3. UI组件库: Element Plus。它是基于Vue 3的桌面端组件库,成熟、美观、文档齐全。我们的后台管理系统界面可以大量使用它的表格、表单、弹窗等组件快速搭建。
  4. 路由: Vue Router 4。
  5. 状态管理: Pinia。这是Vue官方推荐的新一代状态管理库,比Vuex更简洁,TypeScript支持更好。我们可以用它来管理用户登录状态、播放历史等全局数据。
  6. HTTP客户端: Axios。用于发起对后端API的请求,配合拦截器(Interceptor)可以统一处理Token添加、错误响应等。
  7. 视频播放器: 这是前端核心。经过对比,我选择了video.js配合videojs-contrib-hls插件。原因如下:
    • video.js功能强大,UI可定制性高,社区插件丰富。
    • 对于HLS(.m3u8)格式的视频流支持非常好,这是目前网络视频播放的主流格式,能实现自适应码率和流畅播放。
    • 虽然Vue也有像vue-video-player这样的封装,但直接使用video.js能获得更底层的控制权,方便处理一些高级需求(比如自定义控制栏、插播广告等)。

2.3 前后端分离架构与部署考量

我们采用经典的前后端分离架构。前端Vue项目通过Vite打包后,生成静态资源(HTML, JS, CSS, 图片等)。后端SpringBoot项目提供REST API。生产环境部署有两种常见模式:

  1. 完全分离部署:前端静态资源部署在Nginx或CDN上,后端Java应用部署在Tomcat或直接使用SpringBoot内嵌容器。这种模式前后端完全解耦,可以独立伸缩。
  2. 合并部署:将Vite打包后的dist目录下的静态文件,复制到SpringBoot项目的src/main/resources/static目录下。SpringBoot应用同时充当API服务器和静态资源服务器。这种模式部署简单,适合小型项目或个人项目。

我这次分享的示例将采用第二种“合并部署”模式,因为它更易于演示和本地运行。但在实际生产环境中,尤其是稍有规模的项目,我强烈推荐第一种“完全分离部署”。

3. 后端核心功能实现详解

3.1 数据库设计与实体类映射

视频网站的核心数据表并不复杂,但设计时需要考虑扩展性。以下是核心表结构:

-- 视频信息表 CREATE TABLE `video` ( `id` bigint PRIMARY KEY AUTO_INCREMENT COMMENT '主键', `title` varchar(255) NOT NULL COMMENT '视频标题', `description` text COMMENT '视频描述', `cover_url` varchar(500) COMMENT '封面图URL', `video_url` varchar(500) COMMENT '原始视频文件存储路径(如OSS的Key)', `m3u8_url` varchar(500) COMMENT '转码后的HLS索引文件(.m3u8)路径', `duration` int DEFAULT 0 COMMENT '视频时长(秒)', `status` tinyint DEFAULT 1 COMMENT '状态(0:转码中, 1:正常, 2:下架)', `category_id` bigint COMMENT '分类ID', `user_id` bigint COMMENT '上传用户ID', `view_count` bigint DEFAULT 0 COMMENT '播放次数', `like_count` bigint DEFAULT 0 COMMENT '点赞数', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX `idx_category` (`category_id`), INDEX `idx_user` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='视频信息表'; -- 视频分类表 CREATE TABLE `category` ( `id` bigint PRIMARY KEY AUTO_INCREMENT, `name` varchar(100) NOT NULL, `parent_id` bigint DEFAULT 0 COMMENT '父级ID,用于多级分类', `sort` int DEFAULT 0 COMMENT '排序', `create_time` datetime DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 用户表(简化版) CREATE TABLE `user` ( `id` bigint PRIMARY KEY AUTO_INCREMENT, `username` varchar(50) UNIQUE NOT NULL, `password` varchar(255) NOT NULL COMMENT '加密后的密码', `nickname` varchar(100), `avatar` varchar(500), `role` varchar(20) DEFAULT 'USER' COMMENT '角色:ADMIN, USER', `create_time` datetime DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

对应的,在SpringBoot项目中,我们使用MyBatis-Plus的注解来定义实体类。以Video实体为例:

import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.time.LocalDateTime; @Data @TableName("video") public class Video { @TableId(type = IdType.AUTO) private Long id; private String title; private String description; private String coverUrl; private String videoUrl; // 原始文件路径 private String m3u8Url; // HLS文件路径 private Integer duration; private Integer status; private Long categoryId; private Long userId; private Long viewCount; private Long likeCount; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; // 非数据库字段,用于关联查询 @TableField(exist = false) private String categoryName; @TableField(exist = false) private String uploaderName; }

这里我使用了@TableField(fill = FieldFill.INSERT)等注解,配合MyBatis-Plus的元数据处理器(MetaObjectHandler),可以自动填充创建和更新时间,避免手动设置。

3.2 视频上传与云存储集成

直接让用户上传视频到应用服务器是极不推荐的,这会给服务器带来巨大的磁盘I/O和带宽压力。主流做法是集成对象存储服务,如阿里云OSS、腾讯云COS、七牛云Kodo等。它们提供高可用、高扩展、低成本的存储,并且自带CDN加速。

以阿里云OSS为例,集成步骤:

  1. 引入OSS的Java SDK依赖。
  2. 在配置文件中配置OSS的Endpoint、AccessKey、Bucket名称等信息。
  3. 创建一个FileStorageService服务类,封装上传、删除、生成签名URL等方法。

核心上传逻辑:

@Service public class OssStorageService implements FileStorageService { @Value("${oss.bucket-name}") private String bucketName; @Autowired private OSS ossClient; @Override public String upload(InputStream inputStream, String fileName) { // 生成一个在OSS中唯一的对象键(Key),避免文件名冲突 // 常用格式:video/{yyyy}/{MM}/{dd}/{uuid}_{originalFileName} String key = generateFileKey(fileName); try { // 创建上传对象的元信息 ObjectMetadata metadata = new ObjectMetadata(); metadata.setContentType(getContentType(fileName)); // 如果是大文件,可以考虑使用分片上传(UploadFileRequest) ossClient.putObject(bucketName, key, inputStream, metadata); // 返回文件的访问URL(可以设置过期时间,或者直接返回永久URL如果Bucket是公共读) return generateUrl(key); } catch (Exception e) { throw new RuntimeException("文件上传至OSS失败", e); } } private String generateFileKey(String originalFileName) { String uuid = UUID.randomUUID().toString().replace("-", ""); String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")); String extension = originalFileName.substring(originalFileName.lastIndexOf(".")); return String.format("video/%s/%s%s", datePath, uuid, extension); } }

实操心得:务必在服务端生成最终的文件存储路径(Key),永远不要信任客户端上传的文件名,这可以防止文件名冲突、路径遍历攻击以及特殊字符导致的问题。同时,按日期分目录存储有利于后期管理和迁移。

3.3 视频转码与HLS生成服务

用户上传的原始视频格式(如MP4、MOV)和编码参数千差万别,为了确保在所有浏览器和设备上都能流畅播放,我们需要将视频转码成标准的、支持自适应码率的格式。HLS(HTTP Live Streaming)是目前最通用的方案。

我们不可能在SpringBoot应用内直接进行耗时的视频转码,这会阻塞主线程。正确的做法是:

  1. 用户上传视频到OSS后,后端记录一条视频记录,状态为“转码中”。
  2. 后端向一个异步任务队列(如RabbitMQ、RocketMQ)发送一条转码任务消息。
  3. 一个独立的转码服务(Worker)监听这个队列,获取任务。
  4. 转码服务从OSS下载原始视频,使用FFmpeg进行转码,生成多码率的.ts分片文件和.m3u8索引文件。
  5. 转码完成后,将生成的HLS文件上传回OSS(或另一个Bucket),并回调通知SpringBoot服务更新视频状态和m3u8_url

这里给出一个简化的转码任务消息体设计和FFmpeg命令示例:

// 转码任务消息 @Data public class TranscodeTask { private Long videoId; private String sourceFileUrl; // 原始文件OSS URL private String targetM3u8Key; // 目标m3u8文件在OSS的Key }
# 一个基本的FFmpeg命令,将输入视频转码为HLS # 生成三种清晰度:1080p, 720p, 480p ffmpeg -i input.mp4 \ -map 0:v:0 -map 0:a:0 \ -c:v libx264 -crf 22 -c:a aac -ar 44100 \ -vf "scale=w=1920:h=1080:force_original_aspect_ratio=decrease" -b:v 5000k -maxrate 5350k -bufsize 7500k -hls_time 10 -hls_playlist_type vod -hls_segment_filename "output_1080p_%03d.ts" output_1080p.m3u8 \ -map 0:v:0 -map 0:a:0 \ -c:v libx264 -crf 23 -c:a aac -ar 44100 \ -vf "scale=w=1280:h=720:force_original_aspect_ratio=decrease" -b:v 2500k -maxrate 2675k -bufsize 3750k -hls_time 10 -hls_playlist_type vod -hls_segment_filename "output_720p_%03d.ts" output_720p.m3u8 \ -map 0:v:0 -map 0:a:0 \ -c:v libx264 -crf 23 -c:a aac -ar 44100 \ -vf "scale=w=854:h=480:force_original_aspect_ratio=decrease" -b:v 1000k -maxrate 1070k -bufsize 1500k -hls_time 10 -hls_playlist_type vod -hls_segment_filename "output_480p_%03d.ts" output_480p.m3u8

注意事项:FFmpeg参数调优是个深水区,需要根据实际视频内容和目标质量平衡码率、CRF值等。在生产环境,转码服务最好部署在带GPU的机器上,或者直接使用云厂商提供的视频处理服务(如阿里云MPS、腾讯云VOD),它们封装了这些复杂操作,虽然成本更高,但稳定性和效率有保障。

3.4 RESTful API设计与实现

API设计要遵循RESTful风格,清晰易懂。以下是一些核心接口设计:

模块请求方法路径描述权限
视频GET/api/videos分页查询视频列表公开
视频GET/api/videos/{id}获取视频详情(播放地址)公开
视频POST/api/videos上传视频信息(触发转码)管理员
视频PUT/api/videos/{id}更新视频信息管理员
视频DELETE/api/videos/{id}删除视频管理员
分类GET/api/categories获取全部分类树公开
用户POST/api/auth/login用户登录公开
用户POST/api/auth/register用户注册公开
用户GET/api/users/me获取当前用户信息用户

在SpringBoot中,我们使用@RestController@RequestMapping等注解来定义控制器。以视频列表接口为例:

@RestController @RequestMapping("/api/videos") public class VideoController { @Autowired private VideoService videoService; @GetMapping public Result<Page<VideoVO>> listVideos( @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String keyword, @RequestParam(required = false) Long categoryId) { Page<Video> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Video> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.like(StringUtils.isNotBlank(keyword), Video::getTitle, keyword) .eq(categoryId != null && categoryId > 0, Video::getCategoryId, categoryId) .eq(Video::getStatus, 1) // 只查询正常状态的视频 .orderByDesc(Video::getCreateTime); Page<Video> videoPage = videoService.page(page, queryWrapper); // 将Page<Video>转换为Page<VideoVO>,并填充关联信息(如分类名) Page<VideoVO> voPage = videoService.convertToVoPage(videoPage); return Result.success(voPage); } }

这里我使用了MyBatis-Plus的Page对象进行分页,LambdaQueryWrapper构建查询条件,代码非常简洁。返回的Result是一个统一响应包装类,包含codemsgdata字段,便于前端统一处理。

3.5 安全与权限控制实现

使用Spring Security + JWT进行安全控制是行业标准做法。核心配置步骤如下:

  1. 创建JWT工具类:用于生成Token、解析Token、校验Token有效性。
  2. 实现UserDetailsService:从数据库加载用户信息,用于认证。
  3. 配置SecurityFilterChain:这是Spring Security 5.7+推荐的方式(替代了旧的WebSecurityConfigurerAdapter)。
@Configuration @EnableWebSecurity public class SecurityConfig { @Autowired private JwtTokenProvider jwtTokenProvider; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // 禁用CSRF,因为我们是无状态的API,使用JWT .csrf().disable() // 设置Session管理为无状态 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() // 配置请求授权规则 .authorizeHttpRequests(authz -> authz // 公开接口 .requestMatchers("/api/auth/**", "/api/videos", "/api/videos/{id}", "/api/categories").permitAll() // 静态资源(前端打包后的文件) .requestMatchers("/", "/index.html", "/assets/**", "/vite.svg").permitAll() // 管理员接口 .requestMatchers("/api/videos", "/api/videos/**").hasRole("ADMIN") // 其他所有请求都需要认证 .anyRequest().authenticated() ) // 添加我们自定义的JWT过滤器 .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class) // 异常处理 .exceptionHandling() .authenticationEntryPoint(new JwtAuthenticationEntryPoint()) // 未登录处理 .accessDeniedHandler(new JwtAccessDeniedHandler()); // 权限不足处理 return http.build(); } // 密码编码器,使用BCrypt @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }

自定义的JwtAuthenticationFilter会拦截请求,从Header中取出Token进行验证,如果有效,则将用户信息设置到Spring Security的上下文中,供后续控制器使用。

4. 前端核心功能实现详解

4.1 项目初始化与基础配置

使用Vite快速初始化一个Vue 3 + TypeScript项目:

npm create vue@latest my-video-site # 按照提示选择:TypeScript, Vue Router, Pinia, ESLint等 cd my-video-site npm install

安装核心依赖:

npm install element-plus axios pinia video.js videojs-contrib-hls # 安装图标库(可选) npm install @element-plus/icons-vue

配置Vite,设置代理解决开发环境跨域:

// vite.config.ts import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { resolve } from 'path' export default defineConfig({ plugins: [vue()], resolve: { alias: { '@': resolve(__dirname, 'src') } }, server: { port: 3000, // 前端开发服务器端口 proxy: { '/api': { target: 'http://localhost:8080', // 后端SpringBoot地址 changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } } })

4.2 集成Video.js播放器组件

创建一个可复用的视频播放器组件VideoPlayer.vue

<template> <div class="video-player-container"> <div ref="videoContainer"></div> </div> </template> <script setup lang="ts"> import { ref, onMounted, onUnmounted, watch } from 'vue' import videojs from 'video.js' import 'video.js/dist/video-js.css' import 'videojs-contrib-hls' // 引入HLS插件 interface Props { src: string // m3u8地址 poster?: string // 封面图 options?: any // 额外的video.js配置 } const props = withDefaults(defineProps<Props>(), { options: () => ({}) }) const videoContainer = ref<HTMLElement>() let player: any = null const initPlayer = () => { if (!videoContainer.value) return // 销毁旧的播放器实例 if (player) { player.dispose() } // 合并默认配置和传入的配置 const videoOptions = { controls: true, autoplay: false, preload: 'auto', fluid: true, // 自适应容器宽度 sources: [{ src: props.src, type: 'application/x-mpegURL' // HLS的MIME类型 }], poster: props.poster, ...props.options } player = videojs(videoContainer.value, videoOptions, function onPlayerReady() { console.log('播放器准备就绪') // 可以在这里监听播放器事件 this.on('error', (e: any) => { console.error('视频播放错误:', e) }) this.on('ended', () => { console.log('视频播放结束') // 可以触发播放完成的后端记录逻辑 }) }) } // 监听src变化,重新初始化播放器 watch(() => props.src, (newSrc) => { if (newSrc) { // 这里可以添加一个延迟,确保DOM更新完毕 setTimeout(initPlayer, 100) } }) onMounted(() => { if (props.src) { initPlayer() } }) onUnmounted(() => { if (player) { player.dispose() } }) </script> <style scoped> .video-player-container { width: 100%; max-width: 1200px; margin: 0 auto; } </style>

实操心得:video.js的初始化必须在DOM元素挂载完成后进行。在Vue的<script setup>中,使用onMounted生命周期钩子来确保这一点。另外,当视频源(src)动态变化时(比如切换清晰度),需要先销毁旧的播放器实例再创建新的,否则可能会产生冲突或内存泄漏。

4.3 状态管理与API请求封装

使用Pinia创建全局状态管理store。例如,创建一个userStore来管理用户登录状态:

// stores/user.ts import { defineStore } from 'pinia' import { ref, computed } from 'vue' import type { UserInfo } from '@/types/user' import { loginApi, getUserInfoApi, logoutApi } from '@/api/user' export const useUserStore = defineStore('user', () => { const token = ref<string | null>(localStorage.getItem('token')) const userInfo = ref<UserInfo | null>(null) const isLogin = computed(() => !!token.value) const login = async (username: string, password: string) => { const res = await loginApi({ username, password }) token.value = res.data.token userInfo.value = res.data.userInfo localStorage.setItem('token', token.value) // 可以在这里将token设置到axios的默认请求头中 } const fetchUserInfo = async () => { if (!token.value) return try { const res = await getUserInfoApi() userInfo.value = res.data } catch (error) { // 如果获取用户信息失败(如token过期),则清空状态 logout() } } const logout = () => { token.value = null userInfo.value = null localStorage.removeItem('token') // 清除axios默认请求头中的token } return { token, userInfo, isLogin, login, fetchUserInfo, logout } })

封装Axios实例,统一处理请求拦截(添加Token)、响应拦截(处理错误)和基础配置:

// utils/request.ts import axios from 'axios' import { useUserStore } from '@/stores/user' import { ElMessage } from 'element-plus' const service = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || '/api', timeout: 10000 }) // 请求拦截器 service.interceptors.request.use( (config) => { const userStore = useUserStore() if (userStore.token) { config.headers.Authorization = `Bearer ${userStore.token}` } return config }, (error) => { return Promise.reject(error) } ) // 响应拦截器 service.interceptors.response.use( (response) => { const res = response.data // 假设后端统一返回格式为 { code: number, msg: string, data: any } if (res.code === 200) { return res } else { // 业务逻辑错误 ElMessage.error(res.msg || '请求失败') return Promise.reject(new Error(res.msg || 'Error')) } }, (error) => { // HTTP状态码错误 if (error.response?.status === 401) { // 未授权,跳转到登录页 const userStore = useUserStore() userStore.logout() ElMessage.warning('登录已过期,请重新登录') // 可以使用router进行跳转,注意避免循环依赖 window.location.href = '/login' } else if (error.response?.status === 403) { ElMessage.error('权限不足') } else { ElMessage.error(error.message || '网络错误') } return Promise.reject(error) } ) export default service

4.4 视频列表页与播放页开发

视频列表页主要使用Element Plus的ElRowElColElCard组件进行布局,通过分页组件ElPagination加载数据。

<!-- VideoList.vue 部分代码 --> <template> <div class="video-list-page"> <div class="filter-bar"> <el-input v-model="searchKeyword" placeholder="搜索视频..." @keyup.enter="handleSearch" /> <el-select v-model="selectedCategory" placeholder="全部分类" @change="handleCategoryChange"> <el-option label="全部分类" :value="null" /> <el-option v-for="cat in categories" :key="cat.id" :label="cat.name" :value="cat.id" /> </el-select> </div> <el-row :gutter="20"> <el-col v-for="video in videoList" :key="video.id" :xs="24" :sm="12" :md="8" :lg="6"> <el-card class="video-card" @click="goToDetail(video.id)"> <img :src="video.coverUrl" class="video-cover" alt="封面" /> <div class="video-info"> <h3 class="title">{{ video.title }}</h3> <div class="meta"> <span><el-icon><View /></el-icon> {{ video.viewCount }}</span> <span><el-icon><Clock /></el-icon> {{ formatDuration(video.duration) }}</span> </div> </div> </el-card> </el-col> </el-row> <div class="pagination"> <el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :total="total" :page-sizes="[12, 24, 48]" layout="total, sizes, prev, pager, next, jumper" @size-change="handleSizeChange" @current-change="handleCurrentChange" /> </div> </div> </template> <script setup lang="ts"> import { ref, onMounted } from 'vue' import { useRouter } from 'vue-router' import { getVideoListApi } from '@/api/video' import type { VideoItem } from '@/types/video' const router = useRouter() const videoList = ref<VideoItem[]>([]) const total = ref(0) const currentPage = ref(1) const pageSize = ref(12) const searchKeyword = ref('') const selectedCategory = ref<number | null>(null) const loadVideoList = async () => { const res = await getVideoListApi({ pageNum: currentPage.value, pageSize: pageSize.value, keyword: searchKeyword.value, categoryId: selectedCategory.value }) videoList.value = res.data.records total.value = res.data.total } const goToDetail = (id: number) => { router.push(`/video/${id}`) } onMounted(() => { loadVideoList() }) </script>

播放页VideoDetail.vue则主要包含播放器组件、视频信息、推荐列表等。关键点在于从路由参数中获取视频ID,然后调用接口获取视频详情(包含m3u8_url),最后传递给播放器组件。

4.5 大文件分片上传组件实现

对于视频上传,必须实现分片上传以支持大文件、断点续传和网络稳定性。我们可以使用Element Plus的ElUpload组件,并自定义上传行为。

核心思路:

  1. 前端将文件切割成固定大小的分片(如5MB)。
  2. 为每个分片计算MD5或SHA-256哈希(用于标识和秒传)。
  3. 调用后端接口检查哪些分片已上传(实现断点续传)。
  4. 并发上传未完成的分片。
  5. 所有分片上传完成后,通知后端合并分片。

由于代码较长,这里给出关键步骤的伪代码和思路:

// 1. 文件分片 const chunkSize = 5 * 1024 * 1024 // 5MB const chunks: Blob[] = [] let start = 0 while (start < file.size) { const chunk = file.slice(start, start + chunkSize) chunks.push(chunk) start += chunkSize } // 2. 计算文件整体和每个分片的哈希(可以使用spark-md5库) const fileHash = await calculateFileHash(file) const chunkHashes = await Promise.all(chunks.map(chunk => calculateChunkHash(chunk))) // 3. 调用后端预检接口,传递fileHash和chunkHashes // 后端返回已上传成功的分片索引列表 const { uploadedIndexes } = await checkChunks(fileHash, chunkHashes) // 4. 上传未完成的分片 const uploadPromises = chunks .filter((_, index) => !uploadedIndexes.includes(index)) .map((chunk, index) => uploadChunk(fileHash, index, chunk, chunkHashes[index])) await Promise.all(uploadPromises) // 5. 通知后端合并分片 await mergeChunks(fileHash, file.name, chunks.length)

后端需要提供三个接口:/api/upload/check(检查分片)、/api/upload/chunk(上传分片)、/api/upload/merge(合并分片)。合并时,使用Java的NIO(Files.createFileFileChannel)将临时分片文件按顺序合并成最终文件,效率更高。

5. 项目部署与运维要点

5.1 前端构建与后端打包

前端构建: 在项目根目录运行npm run build,Vite会在dist目录下生成优化后的静态资源。为了与SpringBoot集成,我们需要将这些文件复制到SpringBoot项目的src/main/resources/static目录下。可以写一个简单的脚本或使用Maven插件(如frontend-maven-plugin)在打包阶段自动完成。

后端打包: 使用Spring Boot Maven插件生成可执行的JAR文件。

mvn clean package -DskipTests

打包后会在target目录下生成一个your-project-0.0.1-SNAPSHOT.jar文件。这个JAR包内嵌了Tomcat服务器,可以直接运行。

5.2 生产环境配置与优化

  1. 配置文件分离:使用application-prod.yml来覆盖开发环境的配置,如数据库连接、OSS密钥、日志级别等。通过启动参数--spring.profiles.active=prod激活。
  2. 数据库连接池调优:使用HikariCP,根据实际并发量调整maximum-pool-sizeconnection-timeout等参数。
  3. JVM参数优化:在启动JAR时指定JVM参数,例如设置堆内存大小、垃圾回收器等。
    java -Xms512m -Xmx1024m -XX:+UseG1GC -jar your-project.jar --spring.profiles.active=prod
  4. 静态资源缓存:在SpringBoot配置中,可以为静态资源设置HTTP缓存头,减少重复请求。
    # application-prod.yml spring: web: resources: cache: cachecontrol: max-age: 365d public: true # 针对带哈希的文件名(如main.abc123.js)设置永久缓存 chain: strategy: content: enabled: true paths: /**

5.3 使用Nginx进行反向代理与负载均衡

在生产环境,更推荐使用Nginx作为反向代理服务器。将前端静态文件交由Nginx直接服务,将API请求代理到后端的SpringBoot应用。这样性能更好,也便于配置SSL、Gzip压缩等。

一个简单的Nginx配置示例:

server { listen 80; server_name your-domain.com; # 重定向到HTTPS(如果有SSL证书) # return 301 https://$server_name$request_uri; # 前端静态文件 location / { root /path/to/your/vue/dist; index index.html; try_files $uri $uri/ /index.html; # 支持Vue Router的history模式 } # 后端API代理 location /api/ { proxy_pass http://127.0.0.1:8080/; # SpringBoot应用地址 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 如果后端服务需要较长时间处理(如大文件上传),需要调整超时时间 proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; } # 开启Gzip压缩 gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; }

5.4 常见问题排查与性能优化

问题1:视频播放卡顿或加载慢

  • 排查:首先检查网络。其次,查看浏览器开发者工具的Network面板,观察.m3u8.ts文件的加载情况。如果.ts文件加载时间过长,可能是服务器带宽不足或CDN未生效。
  • 解决
    • 确保视频已正确转码为多码率HLS,播放器能根据网速自适应切换。
    • 将HLS文件(.m3u8.ts)放置在CDN上。
    • 检查Nginx或对象存储服务是否对视频文件正确配置了Content-Typevideo/MP2Tfor .ts,application/vnd.apple.mpegurlfor .m3u8)。

问题2:大文件上传失败

  • 排查:检查浏览器控制台和服务器日志。常见原因是Nginx或SpringBoot的请求大小限制、请求超时限制。
  • 解决
    • Nginx:在配置文件中增加client_max_body_size 100m;和超时时间。
    • SpringBoot:在application.yml中配置:
      spring: servlet: multipart: max-file-size: 100MB max-request-size: 100MB
    • 实现分片上传,这是解决大文件上传最稳健的方式。

问题3:前端路由在刷新后404(History模式)

  • 解决:确保Web服务器(如Nginx)为所有非静态文件请求都返回index.html,这就是上面Nginx配置中try_files $uri $uri/ /index.html;的作用。

问题4:数据库连接数耗尽

  • 排查:查看应用日志,是否有大量数据库连接超时的错误。使用监控工具(如Druid连接池的监控界面)查看活跃连接数。
  • 解决
    • 优化连接池配置(maximum-pool-size不要设置过大)。
    • 检查代码中是否有数据库连接未正确关闭的情况(MyBatis-Plus通常会自动管理)。
    • 对于复杂的查询,确保数据库表已建立合适的索引。

性能优化建议

  1. 数据库层面:为video表的category_id,status,create_time等常用查询字段建立索引。避免SELECT *,只查询需要的字段。
  2. 应用层面:对热点数据(如首页视频列表)使用Redis进行缓存。注意设置合理的过期时间和缓存更新策略(如删除更新)。
  3. 静态资源:务必开启Nginx的Gzip压缩,并对图片、视频等资源设置长期缓存(通过文件名哈希实现)。
  4. 视频服务:强烈建议将视频转码和存储完全托管给云服务商的对象存储和媒体处理服务,自己只管理元数据。这能节省大量服务器成本和运维精力。

整个项目从技术选型到部署上线的链路比较长,每个环节都有需要注意的细节。建议在开发时,先打通核心流程(上传->转码->播放),再逐步完善用户管理、评论、点赞等周边功能。遇到问题多查看官方文档和社区讨论,大部分坑都有前人踩过。

http://www.cnnetsun.cn/news/3720643.html

相关文章:

  • Verilog实现Sobel边缘检测:FPGA图像处理流水线设计实战
  • 合泰单片机IO口操作实战:从寄存器配置到LED与按键驱动
  • 步进电机从原理到实战:选型、驱动与控制全解析
  • Unity项目迁移与依赖管理:从版本兼容到成功运行的完整指南
  • LeetCode 第42题 接雨水
  • Android Fastboot命令全解析:从原理到实战,解锁设备底层控制权
  • 从按键消抖到状态机:嵌入式GPIO输入与事件驱动设计实战
  • 全球拼图式停车系统市场发展模式及前景战略分析报告2026年版
  • GraphRAG 和 LightRAG 详解:原理、对比与选型
  • Java集合框架:ArrayList创建方式全解析与性能优化实践
  • 黑客圈都在聊什么,带你盘点全球十大知名安全社区
  • 【AI媒体内容生产终极指南】:20年实战总结的7大避坑法则与3步提效公式
  • 从零搭建AI Agent:基于LangChain与RAG的工程实践指南
  • 技术提问九大准则:从无效沟通到高效协作的实践指南
  • 计算机毕业设计之基于SpringBoot的地铁站点查询系统
  • 20260728 交付文档定稿与音频子系统理解
  • 导电墨水笔电路制作:从原理到实践,手绘电子原型全解析
  • LLM工具实战指南:从环境适配到批量任务部署
  • AI Agent开发实战:从基础对话到企业级多步骤任务规划
  • Keras深度学习训练范式:构建模型 (Build)→ 配置训练规则 (Compile)→ 执行训练(Fit) + 回调控制(Callback)
  • 植物冠层参数解析:从LAI到FAPAR,量化植被生产力的关键技术
  • Python os模块深度解析:从文件操作到系统交互的实战指南
  • 5分钟免费获取11款米哈游游戏字体:HoYo-Glyphs完整使用指南
  • 从零手写一个 ReAct Agent:让大模型自己调用工具
  • 华为MetaERP Oracle EBS 离散制造:工单、BOM、车间领料、完工入库、五大成本要素、成本中心核算,从设计哲学 → 核心模型 → 五大成本要素 → 业务流程 → 成本中心归集逻辑 → 会
  • 60、80、90、120法兰伺服电机如何匹配行星减速机框号?附计算与接口核对方法
  • 企业级AI Token配额管理:从成本管控到规模化应用实战
  • 麻雀优化算法在PID控制参数整定中的应用实践
  • 基于热释电红外传感器与Arduino的智能安防报警系统DIY全攻略
  • 从DeepSeek融资暂停看技术公司信息安全与风险管理