1. 首先看看效果:
插入不了视频hha,截取录屏,应该能懂我意思吧😁
左右两侧前端代码差异:
左右两侧是请求同一个node接口,前端使用的是 fetch
在左侧对 fetch 返回数据的处理是:response.body.getReader().read去监听response中body数据的每一次数据变化,并直接开始渲染数据
在右侧对fetch 返回的数据处理是:直接等待response.data
在视频中可以明显看出,左侧的数据处理模式可以无需等待全部数据加载完毕便开始渲染,极大提升了大数据传输的用户体验
2. 代码设计
node部分
- 读取要传输的文件数据
- 按照分片大小创建可读流数据
- 监听创建的可读流数据变化,每次变化时将数据写入响应给前端的 res 中 上代码:
exports.getStreamTest = function (req, res) {
const filePath = path.join(__dirname, 'streams') // 同级目录下有一个 streams 文件,里面放着要传输的数据
const fileStats = fs.statSync(filePath); // 获取streams 文件的基本信息
const fileSize = fileStats.size;
const chunkSize = 8; // 由于我测试的文本数据大小较小,所以我将数据分片分的比较小来模拟大文件情况
// 计算需要拆分的块数
const numChunks = Math.ceil(fileSize / chunkSize);
// 设置响应头,表明要进行分块传输
res.setHeader('Content-Type', 'application/octet-stream');
res.setHeader('Content-Length', fileSize.toString());
res.setHeader('Content-Disposition', 'attachment; filename=file.xlsx');
res.setHeader('Transfer-Encoding', 'chunked');
// 使用流式 API 读取数据,并按照顺序逐步返回给前端
let bytesRead = 0;
let currentChunk = 0;
const stream = fs.createReadStream(filePath, {
highWaterMark: chunkSize,
});
stream.on('data', (chunk) => {
console.log(Buffer.isBuffer(chunk)); // 这里的chunk 数据类型是 buffer
bytesRead += chunk.length;
currentChunk++;
// 如果当前块不是最后一块,则在其末尾添加一个分块标记
if (currentChunk < numChunks) {
res.write(chunk + '\r\n');
} else {
res.write(chunk);
res.end();
}
});
stream.on('error', (err) => {
console.error(err);
res.status(500).end();
});
}
前端部分
左侧
async getStream() {
const _this = this
try {
const resData = await fetch('http://127.0.0.1:8888/noToken/largeFile/getStreamTest', {
method: 'POST',
body: JSON.stringify({
'dateawaaa': '111'
})
})
.then(async function (response) {
let MAX_TIME = 2000;
let chunks = [];
const reader = response.body.getReader(); // 创建读取流
let bytesRead = 0;
async function pump() {
if(MAX_TIME){
MAX_TIME--
const { done, value } = await reader.read();
// read()中有两个字段:是否已经全部传输完毕:done、变化的数据:value
if (value) {
bytesRead += value.length;
const textDecoder = new TextDecoder(); // fetch获取的数据是流,这里需要文本解码
const textStream = textDecoder.decode(value, {stream: true});
const getStreamText = document.getElementById('getStream');
await _this.writeStreamText(getStreamText, String.fromCharCode(...JSON.parse(`[${textStream[0]}]`))) // 数据渲染到页面
chunks.push(value); // j将每一片数据存起来,如需要可以在{ done: true}时对全部数据进行处理
}
await pump();
} else{
return false;
}
}
return await pump(); // 流数据变化时递归处理
})
} catch (err) {
console.error(err)
}
},
视频右侧效果代码
normalData() {
axios.post('http://127.0.0.1:8888/noToken/largeFile/getStreamTest').then(res => {
if(res) {
this.writeStreamText(document.getElementById('getNormal'), res.data)
}
});
async writeStreamText(getStreamText,str) {
return new Promise(res => {
setTimeout(() => { // 这里我用的文本数据太少了,接口响应很快,每个分片数据渲染加50ms的延迟让效果更明显一点
getStreamText.innerHTML += str
res();
}, 50);
})
}