11、前端UI框架:React/Vue集成、视频网格布局、画中画模式、响应式设计
做WebRTC多人视频会议,说白了,后端信令和媒体流搞定了,但用户看到的界面一塌糊涂,那这产品就废了。我见过太多项目,技术选型牛得不行,结果UI一打开,视频格子挤成一团,或者某个人的脸占了半个屏幕——用户直接关页面走人。
这一章,咱们就聊聊前端UI怎么落地。我会结合React和Vue两种主流框架,讲讲视频网格布局、画中画模式,还有响应式设计。嗯,这些都是我踩过坑之后总结出来的经验。
核心要点:UI框架的选择决定了开发效率,布局方案决定了用户体验,响应式设计决定了产品能不能在不同设备上活下来。
11.1 React/Vue集成:选谁?怎么搭?
我个人习惯用React做视频会议,因为它的状态管理更灵活,特别是处理多个视频流的动态增删时,React的Hooks用起来很顺手。但Vue也不差,它的模板语法对新手更友好。
先看React的集成方式。你想想看,每个视频流其实就是一个独立的组件,我们需要动态渲染它们。
// React 示例:动态渲染视频流
import React, { useRef, useEffect } from 'react';
const VideoTile = ({ stream, isLocal }) => {
const videoRef = useRef(null);
useEffect(() => {
if (videoRef.current && stream) {
videoRef.current.srcObject = stream;
}
}, [stream]);
return (
<div className={`video-tile ${isLocal ? 'local' : 'remote'}`}>
<video ref={videoRef} autoPlay playsInline muted={isLocal} />
<span className="label">{isLocal ? '我' : '参会者'}</span>
</div>
);
};
// 父组件管理多个视频流
const VideoGrid = ({ streams }) => {
return (
<div className="video-grid">
{streams.map((stream, index) => (
<VideoTile key={stream.id} stream={stream} isLocal={index === 0} />
))}
</div>
);
};
Vue这边呢?其实逻辑差不多,但写法更模板化。我在项目中遇到过一个问题:Vue的响应式系统在处理MediaStream对象时,有时候会触发不必要的重新渲染。解决办法是用shallowRef或者手动控制更新。
<!-- Vue 3 示例:视频瓦片组件 -->
<template>
<div :class="['video-tile', { local: isLocal }]">
<video ref="videoEl" autoplay playsinline :muted="isLocal" />
<span class="label">{{ isLocal ? '我' : '参会者' }}</span>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
const props = defineProps({
stream: Object,
isLocal: Boolean
});
const videoEl = ref(null);
watch(() => props.stream, (newStream) => {
if (videoEl.value && newStream) {
videoEl.value.srcObject = newStream;
}
}, { immediate: true });
</script>
小技巧:不管是React还是Vue,记得给每个视频元素设置playsinline属性。iOS上如果不加这个,视频会强制全屏播放,用户体验极差。我曾经因为这个被测试骂了一下午。
11.2 视频网格布局:让每个人都能被看见
多人视频会议最头疼的就是布局。2个人还好,4个人也凑合,但8个人、16个人呢?你想想看,如果每个人都是固定大小的小方块,那屏幕利用率就太低了。
我常用的方案是动态计算网格。说白了,就是根据参会人数,算出最优的行数和列数,然后让每个视频瓦片自适应填充。
// 动态网格计算函数
function calculateGrid(count, containerWidth, containerHeight) {
// 先算理想的行列数
let cols = Math.ceil(Math.sqrt(count));
let rows = Math.ceil(count / cols);
// 调整比例,避免太扁或太高
while ((cols - 1) * rows >= count) {
cols--;
}
// 计算每个瓦片的尺寸
const tileWidth = containerWidth / cols;
const tileHeight = containerHeight / rows;
return { cols, rows, tileWidth, tileHeight };
}
// React中使用
const GridLayout = ({ streams, containerRef }) => {
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useEffect(() => {
const updateSize = () => {
if (containerRef.current) {
setDimensions({
width: containerRef.current.offsetWidth,
height: containerRef.current.offsetHeight
});
}
};
updateSize();
window.addEventListener('resize', updateSize);
return () => window.removeEventListener('resize', updateSize);
}, []);
const grid = calculateGrid(streams.length, dimensions.width, dimensions.height);
return (
<div ref={containerRef} className="grid-container">
<div className="grid-inner" style={{
gridTemplateColumns: `repeat(${grid.cols}, 1fr)`,
gridTemplateRows: `repeat(${grid.rows}, 1fr)`
}}>
{streams.map(stream => (
<VideoTile key={stream.id} stream={stream} />
))}
</div>
</div>
);
};
注意:当参会人数超过9人时,网格布局会让每个人的画面变得很小。这时候我建议切换到「演讲者模式」——也就是只放大当前说话的人,其他人缩小到侧边栏。这个逻辑我在做企业会议系统时踩过坑,用户反馈说「根本看不清谁在说话」。
11.3 画中画模式:边开会边干别的
画中画(Picture-in-Picture,PiP)是视频会议里一个很实用的功能。用户可以在浏览器里开一个小窗口,把视频会议放在角落,然后继续看文档或者写代码。
WebRTC原生支持PiP API,但要注意兼容性。我建议用document.pictureInPictureEnabled先检测一下浏览器是否支持。
// 开启画中画
async function enablePiP(videoElement) {
if (!document.pictureInPictureEnabled) {
console.warn('浏览器不支持画中画');
return;
}
try {
await videoElement.requestPictureInPicture();
} catch (error) {
console.error('开启画中画失败:', error);
}
}
// 关闭画中画
async function disablePiP() {
if (document.pictureInPictureElement) {
await document.exitPictureInPicture();
}
}
// React组件中集成
const PiPButton = ({ videoRef }) => {
const [isPiP, setIsPiP] = useState(false);
const togglePiP = async () => {
if (isPiP) {
await disablePiP();
setIsPiP(false);
} else {
await enablePiP(videoRef.current);
setIsPiP(true);
}
};
// 监听画中画退出事件(用户可能手动关闭)
useEffect(() => {
const handleLeavePiP = () => setIsPiP(false);
videoRef.current?.addEventListener('leavepictureinpicture', handleLeavePiP);
return () => videoRef.current?.removeEventListener('leavepictureinpicture', handleLeavePiP);
}, []);
return <button onClick={togglePiP}>{isPiP ? '退出画中画' : '画中画'}</button>;
};
避坑指南:我曾经遇到一个问题——画中画窗口里的视频卡住了,但主窗口是正常的。后来发现是因为PiP模式下,浏览器的渲染优先级会降低。解决办法是在开启PiP时,把视频流的帧率降低到15fps,这样能减少CPU压力。
11.4 响应式设计:适配手机、平板、电脑
做视频会议,你不能假设用户都在大屏显示器前。很多人用手机参会,甚至是在地铁上。响应式设计不是可选项,是必选项。
我的策略是:用CSS媒体查询 + JavaScript动态检测,做三档适配。
| 设备类型 | 屏幕宽度 | 布局策略 | 视频瓦片大小 |
|---|---|---|---|
| 手机 | < 768px | 单列滚动,最多显示2个视频 | 100%宽度,自动高度 |
| 平板 | 768px - 1024px | 2列网格,最多显示4个视频 | 50%宽度,16:9比例 |
| 桌面 | > 1024px | 动态网格,最多显示9个视频 | 按网格计算 |
/* CSS 响应式设计 */
.video-grid {
display: grid;
gap: 8px;
padding: 8px;
width: 100%;
height: 100%;
}
/* 手机:单列 */
@media (max-width: 767px) {
.video-grid {
grid-template-columns: 1fr;
grid-template-rows: auto;
}
.video-tile {
min-height: 200px;
}
}
/* 平板:两列 */
@media (min-width: 768px) and (max-width: 1023px) {
.video-grid {
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
}
}
/* 桌面:动态列数由JS控制 */
@media (min-width: 1024px) {
.video-grid {
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}
}
核心原则:响应式设计不是让所有设备显示同样的内容,而是让每个设备显示最适合它的内容。手机上就别想着同时看9个人了,专注当前说话的人才是正道。
知识体系总览
下面这张图是我自己整理的,把这一章的核心知识点串起来了。你看一眼就能明白各个模块之间的关系。
嗯,这一章的内容就这些。UI框架的选择、网格布局的计算、画中画的实现、响应式的适配——每一个点都是我在实际项目中反复打磨过的。你照着做,基本不会出大问题。
最后说一句:技术方案再好,最终还是要落到用户体验上。多站在用户的角度想想——他们开会时最需要什么?是清晰的画面、流畅的操作,还是能在手机上快速加入?想明白了,代码怎么写自然就清楚了。