
使用 PeerJS 构建点对点文件传输 Web 应用
使用 PeerJS 构建点对点文件传输 Web 应用
我希望有一种传送文件且不需要占用服务器磁盘的方法,于是灵犀传出现了,一款基于 PeerJS、QRCode.js 和现代 Web 技术构建的简洁且用户友好的点对点(P2P)文件传输 Web 应用。该应用允许用户在设备间直接传输文件,无需依赖中央服务器,利用 WebRTC 实现快速且安全的传输。
点击后方链接试用:
项目概述
灵犀传 旨在以简约的界面和强大的功能简化文件分享。它支持两种模式:
分享模式:用户选择文件,生成可分享的链接或二维码,然后等待接收者连接。
接收模式:用户粘贴分享链接,连接到分享者并下载文件。
该应用具有以下主要功能:
通过拖放或点击选择文件进行分享。
生成分享链接或二维码,方便跨设备连接。
显示实时传输进度、速度和连接状态。
支持多接收者同时连接(分享者端)。
提供错误处理和网络重试机制,确保可靠的连接。
技术栈
灵犀传 使用了以下技术:
HTML5 和 CSS3:构建响应式和现代化的用户界面。
JavaScript:处理核心逻辑和动态交互。
PeerJS:简化 WebRTC 的实现,用于建立 P2P 连接。
QRCode.js:生成二维码,便于移动设备扫描链接。
WebRTC:通过 ICE 服务器(STUN)实现点对点数据传输。
CSS 变量 和 渐变背景:打造美观的暗色主题界面。
FileReader API:分片读取文件以优化大文件传输。
设计
1. 用户体验优先
灵犀传 的界面设计以简洁和直观为核心:
拖放支持:用户可以拖放文件或点击选择,简化操作。
实时反馈:通过进度条、速度显示和状态通知,让用户随时了解传输情况。
通知系统:使用动画通知提示成功或错误信息。
二维码支持:方便移动设备用户通过扫描快速加入。
2. WebRTC 和 PeerJS
WebRTC 提供了浏览器原生的 P2P 数据传输能力,但其配置较为复杂。PeerJS 通过封装 WebRTC 提供了更简单的 API,用于:
建立信令服务器连接以交换 ICE 候选者。
管理点对点数据通道,传输文件分片。
处理连接错误和断开事件。
为了提高连接成功率,我配置了多个 PeerJS 信令服务器(包括 NTT、PeerJS 官方服务器等)并实现了自动重试机制。如果一个服务器连接失败,应用会切换到下一个服务器,最多重试 3 次。
3. 文件传输优化
为了支持大文件传输,我将文件分片处理:
分片大小:每个分片 256KB,平衡了传输效率和内存使用。
进度更新:每 500 毫秒更新一次进度和速度,避免过于频繁的 DOM 操作。
缓冲区管理:监控 PeerJS 连接的缓冲区大小,避免数据堆积导致性能下降。
4. 错误处理
网络环境复杂,WebRTC 连接可能因防火墙、NAT 穿越失败等原因中断。我实现了以下错误处理机制:
超时检测:接收者在连接分享者时设置 15 秒超时。
重试逻辑:连接失败时自动重试,最多尝试 3 次。
用户提示:通过通知系统显示具体的错误信息(如“对方设备已离线”或“网络错误”)。
实现细节
以下是灵犀传 核心功能的代码解析和实现思路。
1. 初始化 PeerJS
应用启动时初始化 PeerJS 实例,连接到信令服务器:
function initPeer() {
if (peer) peer.destroy();
const server = PEERJS_SERVERS[currentServerIndex];
updateStatus(`正在连接信令服务器... (${currentServerIndex + 1}/${PEERJS_SERVERS.length})`);
try {
peer = new Peer({ ...server, debug: 2, config: ICE_SERVERS });
peer.on('open', id => {
peerIdReady = true;
retryCount = 0;
updateStatus('网络已就绪', false);
});
peer.on('connection', handleNewConnection);
peer.on('error', handlePeerError);
} catch (error) {
handlePeerError(error);
}
}
使用多个信令服务器(
PEERJS_SERVERS
)提高可靠性。配置 STUN 服务器(Google 提供的免费 STUN)以辅助 NAT 穿越。
监听
open
、connection
和error
事件,分别处理连接成功、新连接建立和错误情况。
2. 文件分享流程
分享者选择文件后,生成包含 Peer ID 的分享链接:
function generateShareLink() {
if (!peerIdReady) return showNotification('error', '网络未就绪,请稍等');
const shareLink = `${window.location.origin}${window.location.pathname}?peerId=${peer.id}`;
getEl('shareLink').innerHTML = `<a href="${shareLink}" target="_blank">${shareLink}</a>`;
getEl('shareStep2').classList.remove('hidden');
getEl('shareStep1').style.opacity = "0.5";
updateStatus('已生成链接,等待接收者连接...', true);
}
当接收者连接时,分享者发送文件信息和分片:
function sendFile(conn) {
if (!file) return conn.send({ type: 'error', payload: '分享者未选择文件' });
const CHUNK_SIZE = 256 * 1024;
const reader = new FileReader();
let offset = 0, lastUpdate = Date.now(), lastLoaded = 0;
conn.send({ type: 'file-info', payload: { name: file.name, size: file.size } });
reader.onload = e => {
conn.send({ type: 'file-chunk', payload: e.target.result });
offset += e.target.result.byteLength;
if (offset < file.size) {
if (conn.bufferedAmount > CHUNK_SIZE * 2) setTimeout(sendNextChunk, 100);
else sendNextChunk();
} else {
conn.send({ type: 'file-complete' });
}
};
const sendNextChunk = () => reader.readAsArrayBuffer(file.slice(offset, offset + CHUNK_SIZE));
sendNextChunk();
}
使用
FileReader
按分片读取文件。监控
bufferedAmount
避免数据堆积。发送
file-info
、file-chunk
和file-complete
三种消息类型,分别传递文件元数据、分片数据和完成信号。
3. 文件接收流程
接收者粘贴链接后,解析 Peer ID 并建立连接:
function connectToPeer() {
const url = getEl('shareUrlInput').value;
const match = url.match(/peerId=([a-zA-Z0-9-]+)/);
if (!match) return showNotification('error', '链接格式无效');
updateStatus('正在连接分享者...', true);
activeConn = peer.connect(match[1], { reliable: true });
activeConn.on('open', () => {
getEl('receiveStep2').classList.remove('hidden');
getEl('receiveStep1').classList.add('hidden');
activeConn.send({ type: 'request-file' });
});
}
接收文件分片并保存:
function handleFileData(data) {
if (data.type === 'file-info') {
totalBytes = data.payload.size;
getEl('fileName').textContent = data.payload.name;
getEl('fileSize').textContent = (totalBytes / 1024 / 1024).toFixed(2) + ' MB';
getEl('downloadButton').disabled = false;
} else if (data.type === 'file-chunk') {
receivedBytes += data.payload.byteLength;
receivedChunks.push(data.payload);
const progress = (receivedBytes / totalBytes) * 100;
getEl('downloadProgress').style.width = `${progress}%`;
} else if (data.type === 'file-complete') {
const blob = new Blob(receivedChunks);
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = getEl('fileName').textContent;
a.click();
}
}
收集分片到
receivedChunks
数组。传输完成后,使用
Blob
合并分片并触发下载。
4. 用户界面
界面使用 CSS 变量和现代样式打造:
进度条:使用 CSS 过渡动画平滑显示进度。
通知动画:通过
opacity
和transform
实现淡入淡出效果。
.progress-bar {
height: 8px; background: var(--background-light); border-radius: 4px; overflow: hidden; margin: 20px 0;
}
.progress-fill {
height: 100%; background: var(--primary-color); transition: width 0.3s ease;
}
.notification {
padding: 12px 20px; border-radius: 8px; opacity: 0; transform: translateY(-20px); transition: all 0.3s;
}
.notification.show {
opacity: 1; transform: translateY(0);
}
遇到的问题
网络限制:某些网络(如企业防火墙)可能阻止 WebRTC 连接。
解决方案:提供多个信令服务器和 STUN 服务器,增加连接成功率。
大文件传输:大文件可能导致浏览器内存溢出。
解决方案:分片传输并及时清理缓冲区。
移动端兼容性:部分移动浏览器对 WebRTC 支持不一致。
解决方案:通过二维码简化移动端连接,并测试主流浏览器(如 Chrome、Safari)。
仍然没有实现的
加密传输:为数据通道添加端到端加密,增强安全性。
多文件支持:允许用户一次分享多个文件。
断点续传:记录已传输的分片,支持中途断开后恢复。
自定义 Peer ID:让用户设置易记的 Peer ID,简化连接。
文件预览:在接收端支持图片或视频的预览功能。
总结
灵犀传 展示了如何利用 PeerJS 和 WebRTC 构建一个高效的 P2P 文件传输应用。通过简洁的界面、实时反馈和可靠的错误处理,它为用户提供了流畅的分享体验。这个项目不仅让我深入理解了 WebRTC 的工作原理,还让我在前端设计和性能优化方面积累了宝贵经验。
如果您想尝试灵犀传,可以直接在浏览器中运行代码,或访问 GitHub 仓库 获取源码。欢迎提出建议或贡献代码!
- 感谢你赐予我前进的力量