前言:最近做了一个导出功能,基于Websocket和后端通信,简单记录一下。业务场景是要导出一份文件,文件中带有附件,数据资源可能会很大,后端分成切片返回,前端再组装成完整文件,通过websocket这种长链接方式实现。
WebSocket是什么?
Websocket是基于TCP/IP协议,独立于HTTP协议的通信协议。双向通讯,客户端一个或多个与服务端一个或多个双向实时响应。
业务中如何使用Websocket
建立连接,初始化Websocket
在我的业务场景中,调取后端接口返回某一种状态码时开启链接,创建Websocket。
initSocket() {
// 判断一下用户浏览器是否支持Websocket,websocket的兼容性还是挺好的,具体MDN看一下。
if (typeof WebSocket === 'undefined') {
console.log('您的浏览器不支持websocket');
return;
}
// 传入建立连接地址
this.socket = new WebSocket(`${this.websocketUrl}`);
// websocket四种属性,下面介绍
this.socket.onopen = this.onOpen;
this.socket.onmessage = this.onMesssage;
this.socket.onerror = this.onError;
this.socket.onclose = this.onClose;
},
websocke四种属性如何使用
- onclose:当
websocket
连接关闭时触发; - onerror:当
websocket
连接因为错误而关闭时触发; - onmessage:当
websocket
收到数据时触发; - onopen:当
websocket
连接成功时触发;
根据上面对每个属性的描述可以知道,websocket在什么时候进到哪个方法。在创建websocket
对象建立连接成功后,通过onopen
属性绑定方法发送请求。
onOpen(event) {
console.log('onOpen---', event);
const data = { event: 'add', data: this.form };
this.socket.send(JSON.stringify(data));
},
开启连接后,后端开始数据处理并不断地发送通知信息。在我的业务场景中后端要此时开始下载指定数据的附件信息,并不断的向我发送下载进度。这时候就要通过onMessage
属性来接收后端传递过来的信息。
通信过程如图,绿色箭头代表我发送通知,红色代表我接受到的信息。
在onMessage
属性中更多的是业务处理,通过双方规定的通信数据结构,在接收到不同event事件是进行哪种处理。
onMesssage(message) {
console.log('onMessage---', message);
const { event, data, code, message: msg } = JSON.parse(message.data);
if (event === 'add') {
console.log('开始导出');
} else if (event === 'update-progress') {
console.log('更新进度');
} else if (event === 'ready-download') {
console.log('准备下载');
} else if (event === 'finish-download') {
console.log('下载结束')
}
},
onError
和onClose
事件就是用错误捕获和连接结束时触发,根据业务场景使用。下面到了本文章重点对二进制文件切片处理。
二进制文件切片拼接处理
截图看一下完整的连接过程。在图中可以看到,当我第二次向后端发送信息后,后端开始返回二进制消息。而这些二进制消息是无序的,需要拼接成完整文件格式。下面逐步解析一下从后端准备好文件,到我拼装好文件的过程。
在后端准备好文件后,会返回ready-download的event名称,并且会把文件大小和文件名称一并带回来。此时根据event名称判断,会进入onReadyDownload()
方法中。
在onReadyDownload()
事件中要记录文件名称,计算文件被拆分成几个切片。都是为了后续拼装文件做准备,之后通知后端准备好接收文件了。
onReadyDownload(data) {
this.$message.success('导出数据已就绪,即将自动导出');
this.fileName = data.fileId; // 存储文件名称
this.piece = Math.ceil(data.fileSize / (1024 * 1024)); // 计算切片个数
const tempObj = {.....};
const readyData = { event: 'ready-download', data: tempObj };
this.socket.send(JSON.stringify(readyData)); // 通知后端开始传输吧
},
现在后端开始传输二进制文件,依旧通过onMessage()
事件来捕获,判断后端传输的文件类型是Blob
时存储到数组中去(后面好排序),计算进度。
// onMessage 完整代码
onMesssage(message) {
console.log('onMessage---', message);
if (message.data instanceof Blob) { // 判断是二进制文件
this.myblobs.push(message.data);
this.blobCount += 1; // 计算进度;
this.progress = Math.floor(this.blobCount / this.piece * 100);
return;
}
const { event, data, code, message: msg } = JSON.parse(message.data);
if (event === 'add') {
console.log('更新进度');
} else if (event === 'update-progress') {
console.log('更新进度');
} else if (event === 'ready-download') {
this.onReadyDownload(data);
} else if (event === 'finish-download') {
this.onFinishDownload(data);
}
},
当后端返回finish-download时,表示文件已经转传输完毕,此时断开连接开始对所有二进制信息拼接生成完成文件,进入onFinishDownload()
方法。
在onFinishDownload()
调用handlePieceBlobV2()
方法处理无序二进制信息,再将处理过的信息排序,最后调用handleBlobsV1()
方法创建超链接开始下载。
onFinishDownload(data) {
Promise.all([...this.myblobs.map((blob) => this.handlePieceBlobV2(blob))]).then((results) => {
const blobs = this.sortBlobPiece(results);
this.handleBlobsV1(blobs);
});
}
handlePieceBlobV2()方法
async handlePieceBlobV2(blob) {
const buffer = await blob.slice(0, 8).arrayBuffer(); // 截取前八位切片序号,转buffer
const index = +(new Uint8Array(buffer)).reduce((p, c) => (p + String.fromCharCode(c)), ''); // 字符编码值转对应字符 ; 或者通过blob.text();??
const data = blob.slice(8, 1024 * 1024 + 8); // end -1 和mdn描述的不一样,会缺少1个字节;
return {
index,
data,
};
},
sortBlobPiece()切片排序
sortBlobPiece(arr) {
const res = Array(arr.length);
arr.forEach(({ index, data }) => {
res[index - 1] = data;
});
return res;
},
handleBlobsV1()创建超链接下载
handleBlobsV1(blobs) {
const blob = new Blob([...blobs]);
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.download = this.fileName || Math.random();
a.href = blobUrl;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(blobUrl);
console.log('blobCount', this.blobCount);
},
对于handlePieceBlobV2()
这个方法我还有比较多的疑问,后续有时间会搞清楚arrayBuffer
、Uint8Array
这些对象的使用,以及如何根据前八位切片顺序匹配这些事件,就先记录到这里。