年轻人的第一个React应用:用Node.js构建直播多路监控
本文适合:有一定前端基础,想学习React实战的开发者 技术栈:React + TypeScript + Node.js
前言:为什么要做这个项目?
作为一名云顶之弈爱好者,我经常需要同时观看多个主播的直播来学习不同阵容。但浏览器标签页来回切换实在太麻烦,于是萌生了开发一个多路直播监控工具的想法。

这个项目不仅解决了我的实际需求,还成为了我学习React的绝佳实践。今天就来分享这个项目的完整开发过程。

技术选型与项目搭建
为什么选择React + TypeScript?
bash
# 创建前端项目(React)
npx create-react-app ominiview-monitor -- --template typescript
# 创建后端项目(Nodejs)
npm init
选择React的原因:
- 组件化思维:直播流卡片天然适合组件化开发
- 生态丰富:丰富的UI库和工具链支持
- TypeScript:类型安全,减少运行时错误
核心功能实现
1. 直播流卡片组件设计
typescript
// StreamTile.tsx - 直播流卡片组件
interface StreamTileProps {
stream: StreamSource;
onRemove: (id: string) => void;
onToggleMute: (id: string) => void;
}
const StreamTile: React.FC<StreamTileProps> = ({
stream,
onRemove,
onToggleMute
}) => {
const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);
return (
<div className="stream-tile">
{/* 视频播放区域 */}
<video
src={stream.url}
muted={stream.isMuted}
onLoadedData={() => setIsLoading(false)}
onError={() => setHasError(true)}
/>
{/* 控制工具栏 */}
<div className="controls">
<button onClick={() => onToggleMute(stream.id)}>
{stream.isMuted ? '🔇' : '🔊'}
</button>
<button onClick={() => onRemove(stream.id)}>❌</button>
</div>
</div>
);
};
2. 多布局网格系统
typescript
// 布局枚举定义
export enum GridLayout {
Single = '1x1',
Dual = '2x1',
Triple = '3x1',
Quad = '2x2',
Hex = '3x2'
}
// 动态网格类名生成
const getGridClass = (layout: GridLayout): string => {
const gridMap = {
[GridLayout.Single]: 'grid-cols-1 grid-rows-1',
[GridLayout.Dual]: 'grid-cols-2 grid-rows-1',
[GridLayout.Triple]: 'grid-cols-3 grid-rows-1',
[GridLayout.Quad]: 'grid-cols-2 grid-rows-2',
[GridLayout.Hex]: 'grid-cols-3 grid-rows-2'
};
return gridMap[layout];
};
3. 多平台直播源智能处理
typescript
// URL解析结果处理过程 - 支持虎牙、斗鱼多平台
try {
const response = await fetch('http://localhost:3001/api/parse-url', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ url: customUrl }),
});
if (response.ok) {
const result = await response.json();
console.log('解析结果:', result);
if (result.success && result.data) {
const parsedData = result.data;
// 链接优先级列表(按优先级从高到低)
const urlCandidates: string[] = [];
// 虎牙格式优先级
if (parsedData.links?.flv && typeof parsedData.links.flv === 'object') {
Object.values(parsedData.links.flv).forEach(url => {
if (typeof url === 'string') urlCandidates.push(url);
});
}
if (parsedData.links?.hls && typeof parsedData.links.hls === 'object') {
Object.values(parsedData.links.hls).forEach(url => {
if (typeof url === 'string') urlCandidates.push(url);
});
}
// 斗鱼格式优先级
if (parsedData.links?.pc && typeof parsedData.links.pc === 'string') {
urlCandidates.push(parsedData.links.pc);
}
if (parsedData.links?.mobile && typeof parsedData.links.mobile === 'string') {
urlCandidates.push(parsedData.links.mobile);
}
// CDN链接
if (parsedData.links?.cdnLinks?.m3u8 && Array.isArray(parsedData.links.cdnLinks.m3u8)) {
parsedData.links.cdnLinks.m3u8.forEach(url => {
if (typeof url === 'string') urlCandidates.push(url);
});
}
if (parsedData.links?.cdnLinks?.flv && Array.isArray(parsedData.links.cdnLinks.flv)) {
parsedData.links.cdnLinks.flv.forEach(url => {
if (typeof url === 'string') urlCandidates.push(url);
});
}
// 代理链接
if (parsedData.links?.proxy && typeof parsedData.links.proxy === 'string') {
urlCandidates.push(parsedData.links.proxy);
}
// 原始流URL
if (parsedData.streamUrl && typeof parsedData.streamUrl === 'string') {
urlCandidates.push(parsedData.streamUrl);
}
// 选择第一个有效的链接
if (urlCandidates.length > 0) {
finalUrl = urlCandidates[0];
console.log(`找到 ${urlCandidates.length} 个候选链接,选择:`, finalUrl);
}
finalTitle = parsedData.title || finalTitle;
}
}
}
技术难点与解决方案
难点:FLV格式直播流播放
传统HTML5 video标签不支持FLV格式,我们使用flv.js库来解决:
typescript
import flvjs from 'flv.js';
try {
// 创建FLV播放器实例
// 第一个参数是播放器配置对象
const flvPlayer = flvjs.createPlayer({
type: 'flv', // 指定流媒体类型为FLV格式
url: stream.url, // 设置直播流URL地址
isLive: true // 标记为直播流(非点播)
}, {
// 第二个参数是播放器选项配置
enableWorker: false, // 禁用Web Worker(提高兼容性,但可能影响性能)
lazyLoad: true, // 启用懒加载,延迟加载视频数据
lazyLoadMaxDuration: 3 * 60, // 懒加载最大时长:3分钟
seekType: 'range', // 设置seek类型为range(支持HTTP范围请求)
});
// 将播放器附加到视频DOM元素
flvPlayer.attachMediaElement(videoRef.current);
// 开始加载视频流
flvPlayer.load();
// 开始播放视频
flvPlayer.play();
// 监听加载完成事件
flvPlayer.on(flvjs.Events.LOADING_COMPLETE, () => {
setIsLoading(false); // 隐藏加载状态
setHasError(false); // 清除错误状态
});
// 监听错误事件
flvPlayer.on(flvjs.Events.ERROR, (error) => {
console.error('FLV player error:', error); // 输出错误信息到控制台
setIsLoading(false); // 隐藏加载状态
setHasError(true); // 设置错误状态
});
// 监听元数据到达事件(流媒体信息已获取)
flvPlayer.on(flvjs.Events.METADATA_ARRIVED, () => {
setIsLoading(false); // 隐藏加载状态
setHasError(false); // 清除错误状态
});
// 将播放器实例保存到ref中,便于后续管理和销毁
flvPlayerRef.current = flvPlayer;
} catch (error) {
// 捕获创建播放器过程中的异常
console.error('Failed to create FLV player:', error); // 输出错误信息
setIsLoading(false); // 隐藏加载状态
setHasError(true); // 设置错误状态
}
部署与使用
本地开发
bash
# 安装依赖
npm install
# 启动开发服务器
npm run start
生产构建
bash
npm run build

总结与展望
通过这个项目,不仅掌握了React的核心概念(组件化、状态管理、生命周期),还深入理解了现代前端工程化实践。
技术收获:
- React Hooks的熟练使用
- TypeScript类型系统设计
- 工程化构建配置
未来规划:
- 支持更多直播平台(B站、抖音等)
- 添加录制功能
- 开发浏览器插件版本
给新手的建议
- 从实际需求出发:解决真实问题能让学习更有动力
- 循序渐进:先实现核心功能,再逐步优化
- 善用工具:gemini3、豆包等工具能提升开发效率
- 代码规范:从一开始就养成良好的编码习惯
源码地址
项目已开源,欢迎Star和贡献: 👉 GitHub仓库地址
如果文章对你有帮助,欢迎点赞收藏~有任何问题可以在评论区交流!