1. 项目背景与需求分析
最近在给公司搭建内部培训视频平台时,我选择了Node.js+Vue+ElementUI这套技术栈。这个选择并非偶然,而是基于以下几个实际需求:
-
跨部门协作需求:公司有销售、技术、运营等多个部门,每个季度都需要进行产品知识、技能提升等培训。传统线下培训效率低,且难以追踪学习效果。
-
视频管理痛点:培训视频分散在各个U盘、网盘中,版本混乱,新员工难以获取完整资料。
-
学习数据缺失:管理层无法实时掌握员工学习进度,HR难以评估培训效果。
经过技术选型评估,最终方案确定为:
- 后端:Node.js + Express(轻量快速,适合I/O密集型应用)
- 前端:Vue 2.x + ElementUI(开发效率高,组件丰富)
- 数据库:MongoDB(文档型数据库适合存储学习记录)
提示:选择Vue 2.x而非3.x是因为公司现有前端团队更熟悉2.x版本,且ElementUI对2.x支持更完善。如果从零开始建议直接上Vue3+Element Plus。
2. 技术架构设计与核心实现
2.1 后端服务搭建
首先通过Express搭建基础框架:
bash复制# 项目初始化
mkdir training-platform && cd training-platform
npm init -y
npm install express mongoose cors multer ffmpeg-static fluent-ffmpeg
关键模块说明:
multer:处理视频文件上传ffmpeg:视频转码(确保不同设备兼容性)mongoose:MongoDB ODM
核心API设计:
javascript复制// 视频上传接口
app.post('/api/upload', upload.single('video'), async (req, res) => {
try {
const { originalname, path } = req.file
const outputPath = `./converted/${Date.now()}.mp4`
// 转码为通用MP4格式
await new Promise((resolve, reject) => {
ffmpeg(path)
.output(outputPath)
.on('end', resolve)
.on('error', reject)
.run()
})
// 存储到数据库
const video = new Video({
title: originalname,
path: outputPath,
duration: await getVideoDuration(outputPath)
})
await video.save()
res.status(201).json(video)
} catch (err) {
console.error('Upload failed:', err)
res.status(500).send('Upload failed')
}
})
2.2 前端界面开发
使用Vue CLI初始化项目:
bash复制vue create training-frontend
cd training-frontend
vue add element-ui
核心页面结构:
html复制<template>
<el-container>
<el-header>
<el-menu mode="horizontal">
<el-menu-item index="1">学习中心</el-menu-item>
<el-menu-item index="2">我的进度</el-menu-item>
</el-menu>
</el-header>
<el-main>
<video-player
:options="playerOptions"
@ready="onPlayerReady"
@timeupdate="recordProgress"
/>
<el-collapse v-model="activeNames">
<el-collapse-item title="课程列表" name="1">
<el-table :data="videos">
<el-table-column prop="title" label="课程名称"/>
<el-table-column label="操作">
<template #default="scope">
<el-button @click="playVideo(scope.row)">播放</el-button>
</template>
</el-table-column>
</el-table>
</el-collapse-item>
</el-collapse>
</el-main>
</el-container>
</template>
2.3 关键技术问题解决
视频播放兼容性
使用video.js配合hls.js解决m3u8格式支持:
javascript复制import Videojs from 'video.js'
import 'video.js/dist/video-js.css'
import Hls from 'hls.js'
export default {
methods: {
initPlayer() {
if(Hls.isSupported()) {
const hls = new Hls()
hls.loadSource(this.videoUrl)
hls.attachMedia(this.$refs.video)
} else if (this.$refs.video.canPlayType('application/vnd.apple.mpegurl')) {
// Safari原生支持
this.$refs.video.src = this.videoUrl
}
}
}
}
学习进度追踪
通过WebSocket实时上报学习进度:
javascript复制// 前端上报
setInterval(() => {
const progress = this.player.currentTime() / this.player.duration()
this.$socket.emit('progress', {
videoId: this.currentVideo.id,
userId: this.user.id,
progress
})
}, 5000)
// 后端处理
socket.on('connection', (client) => {
client.on('progress', async (data) => {
await Progress.updateOne(
{ userId: data.userId, videoId: data.videoId },
{ $set: { progress: data.progress } },
{ upsert: true }
)
})
})
3. 部署与性能优化
3.1 生产环境部署
使用PM2管理Node进程:
bash复制npm install pm2 -g
pm2 start server.js --name training-platform
Nginx配置示例:
nginx复制server {
listen 80;
server_name training.yourcompany.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
}
location /video/ {
alias /var/www/videos/;
mp4;
mp4_buffer_size 1m;
mp4_max_buffer_size 5m;
}
}
3.2 性能优化措施
- 视频分片加载:
javascript复制// 前端请求时添加Range头
axios.get('/video/1.mp4', {
headers: { 'Range': 'bytes=0-1000000' }
})
// 后端响应
app.get('/video/:id', (req, res) => {
const range = req.headers.range
if (range) {
const [start, end] = range.replace(/bytes=/, "").split("-")
// 返回部分内容
res.writeHead(206, {
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
'Accept-Ranges': 'bytes'
})
createReadStream(videoPath, { start, end }).pipe(res)
}
})
- CDN加速配置:
javascript复制// 七牛云CDN上传示例
const qiniu = require('qiniu')
const mac = new qiniu.auth.digest.Mac(accessKey, secretKey)
const putPolicy = new qiniu.rs.PutPolicy({ scope: bucket })
const uploadToken = putPolicy.uploadToken(mac)
router.post('/upload', async (ctx) => {
const formUploader = new qiniu.form_up.FormUploader()
const putExtra = new qiniu.form_up.PutExtra()
const key = `videos/${Date.now()}${path.extname(ctx.request.files.file.name)}`
await new Promise((resolve) => {
formUploader.putFile(
uploadToken,
key,
ctx.request.files.file.path,
putExtra,
(err, body, info) => {
if (err) throw err
resolve()
}
)
})
ctx.body = { url: `https://cdn.yourcompany.com/${key}` }
})
4. 实际踩坑与解决方案
4.1 Node.js进程崩溃问题
现象:上传大视频时服务频繁崩溃
排查过程:
- 使用
--inspect参数启动调试 - 通过Chrome DevTools发现内存泄漏
- 使用
heapdump模块抓取内存快照
根本原因:未释放FFmpeg进程资源
解决方案:
javascript复制// 修改后的转码逻辑
const ffmpegProcess = ffmpeg(inputPath)
.on('end', () => {
ffmpegProcess.kill() // 显式终止进程
resolve()
})
4.2 ElementUI表格性能问题
当课程超过500条时,页面明显卡顿
优化方案:
- 使用虚拟滚动:
html复制<el-table
:data="videos"
height="500"
:row-height="50"
>
<!-- 列定义 -->
</el-table>
- 后端分页查询:
javascript复制router.get('/videos', async (ctx) => {
const { page = 1, size = 20 } = ctx.query
const videos = await Video.find()
.skip((page - 1) * size)
.limit(Number(size))
ctx.body = videos
})
4.3 视频拖动进度不准确
问题:快速拖动时进度跳变
解决方案:
javascript复制// 使用防抖处理进度上报
let debounceTimer
player.on('timeupdate', () => {
clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
recordProgress(player.currentTime())
}, 1000)
})
5. 扩展功能实现
5.1 弹幕互动功能
前端实现:
javascript复制// 弹幕组件
<template>
<div class="danmu-container">
<div
v-for="(item, index) in danmus"
:key="index"
:style="{
top: `${item.top}px`,
left: `${item.left}px`,
animation: `move ${item.duration}s linear`
}"
>
{{ item.text }}
</div>
<input v-model="newDanmu" @keyup.enter="sendDanmu">
</div>
</template>
<style>
@keyframes move {
to { transform: translateX(-100%); }
}
</style>
后端实时转发:
javascript复制socket.on('danmu', (data) => {
socket.broadcast.emit('new-danmu', {
...data,
time: Date.now()
})
})
5.2 知识测验集成
在视频特定时间点插入测验:
javascript复制// 视频元数据示例
{
"quizPoints": [
{
"time": 120, // 第2分钟
"questions": [
{
"type": "single",
"title": "我们的核心产品优势是?",
"options": ["价格低", "功能全", "服务好"],
"answer": 2
}
]
}
]
}
// 播放器监听
player.on('timeupdate', () => {
const currentQuiz = this.quizPoints.find(
q => Math.abs(q.time - currentTime) < 3
)
if (currentQuiz) this.showQuiz(currentQuiz)
})
6. 安全防护措施
6.1 视频防盗链
Nginx配置:
nginx复制location /videos/ {
valid_referers none blocked server_names
*.yourcompany.com;
if ($invalid_referer) {
return 403;
}
}
6.2 接口鉴权
JWT验证中间件:
javascript复制const auth = async (ctx, next) => {
const token = ctx.headers.authorization?.split(' ')[1]
if (!token) ctx.throw(401)
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET)
ctx.state.user = await User.findById(decoded.userId)
await next()
} catch (err) {
ctx.throw(401, 'Invalid token')
}
}
router.get('/progress', auth, async (ctx) => {
ctx.body = await Progress.find({ userId: ctx.state.user.id })
})
6.3 视频水印添加
使用FFmpeg添加动态水印:
javascript复制ffmpeg(inputPath)
.videoFilters(
`drawtext=text='${user.name}':x=w-tw-10:y=h-th-10:fontsize=24:fontcolor=white@0.5`
)
.output(outputPath)
.run()
7. 监控与运维
7.1 性能监控
使用PM2+Keymetrics:
bash复制pm2 monit
pm2 plus
7.2 日志收集
Winston日志配置:
javascript复制const logger = winston.createLogger({
transports: [
new winston.transports.File({
filename: 'logs/error.log',
level: 'error'
}),
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
})
]
})
// 在中间件中使用
app.use((req, res, next) => {
logger.info(`${req.method} ${req.url}`)
next()
})
7.3 自动备份
MongoDB备份脚本:
bash复制#!/bin/bash
DATE=$(date +%Y%m%d)
mongodump --uri="mongodb://localhost:27017/training" --out=/backups/$DATE
find /backups -type f -mtime +7 -exec rm {} \;
8. 项目总结与展望
经过三个月的开发和迭代,目前平台已经支持:
- 日均500+并发学习
- 2000+小时视频存储
- 98%的视频首屏加载时间<2s
后续规划:
- 引入AI推荐算法,根据员工岗位推荐课程
- 增加直播培训功能
- 开发移动端APP(基于Uniapp)
几个特别值得分享的经验:
- 视频转码一定要在单独的Worker进程中处理
- ElementUI的按需引入能减少30%的打包体积
- 使用IndexedDB缓存学习进度可以提升离线体验
