一、写在前面
ChatGPT 的问答响应界面相信大家都见过,内容是一点一点追加式的显示。不是等好了一起发给你,然后一次性展示出来。这种效果和我们平常开发的展示渲染模式有点区别。可能有的同学会说,前端拿到报文后,我们做成这样的效果不就行行了,有什么难的。
这话看起来很对,但其实不那么对。试想一下,如果一个问答内容响应体很大,几十上百兆,等报文传输完了,我们再显示,中间界面等待的时间会很长,体验其实是很差。
那有什么办法能加速在大报文的场景下,能加速前端界面渲染效果呢?
二、传统式交互
const url ='http://ip:port/path';
async function getResponse(){const resp = await fetch(url,{method : 'GET',});console.log('123');const data = await resp.text();console.log(data);
}
getResponse();
直接从HttpResponseBody中直接一次性获取服务端的响应内容是传统式编程开发。
三、流式传输交互
我们都知道网络传输其实并不是一次性发送所有报文,其实是一部分一部分的传输。也就是说服务端网络是一部分一部分,客户端网络也是一部分一部分接收的,然后拼在一起拿出来的。
那我们有没有办法传输一部分的时候我们就直接一部分的一部分,不用等全部传完我们再拿呢?答案是有的。
const url ='http://ip:port/path';
async function getResponse(){const resp = await fetch(url,{method : 'GET',});// 获取 response 读取器const reader = resp.body.getReader();// 创建一个文本解码器const decoder = new TextDecoder();while (1){// 一块一块读// done 代表读完了没有,value代表此次读的内容是啥const {done,value} = await reader.read();const text = decoder.decode(value)console.log(done);console.log(text)if(done){break;}}
}
如此我们就能提前拿到报文,不需要等报文都传输完再获取。这样加快了前段渲染出数据的速度,而且也达到了ChatGPT那种一点一点追加式显示的效果。
四、监控响应进度
前端实现 Ajax 无非两种做法,一种是XHR,这个API比较老,也比较传统,也是至今用的最广的一种。一种是Fetch,是一种比较新的API。这些都是浏览器的原生能力,我们要知道,原生能力都做不到的话,第三方库也是做不到的。axios 是基于 XHR实现的,umi-request 是基于 Fetch 实现的。原生是能力的边界,第三方库也只能在边界里面玩,这是定数。
上图是这两种 API 原生能力支持的对比说明。
我们现在要实现监控响应进度,那么XHR和Fetch都是能实现的。
如果要做请求进度监控,比如上传进度监控,监控客户端的报文是否都给了服务端,无可厚非,只能选择XHR或者axios。
五、Fetch 监控响应进度实现
const url ='http://ip:port/path';
async function getResponse(){const resp = await fetch(url,{method : 'GET',});const total = +resp.headers.get('content-length');let body = '';let loaded = 0;// 获取 response 读取器const reader = resp.body.getReader();// 创建一个文本解码器const decoder = new TextDecoder();console.log(total);while (1){// 一块一块读// done 代表读完了没有,value代表此次读的内容是啥const {done,value} = await reader.read();if(done){break;}loaded += value.length;const text = decoder.decode(value)body += textconsole.log(loaded);// console.log(body);// console.log(done);// console.log(text)}
}
getResponse();
上面方法是通过读取response的header头中content-length 属性获取报文总大小,然后计算每次获取的量,这样就达到了监控响应进度的目的。
六、XHR 监控响应进度实现
const url ='http://ip:port/path';
async function getResponse(){const xhr = new XMLHttpRequest();// 监听读取内容状态变更事件xhr.addEventListener('readystatechange',()=>{// 当都读取状态为结束时if(xhr.readyState === xhr.DONE){// 完整报文console.log(xhr.responseText);}});// 监听进度事件xhr.addEventListener('progress',(e)=>{console.log(e);});xhr.open('GET',url);xhr.send();
}
getResponse();
XHR是事件类型的 API 风格,是通过监听 xhr 不同的事件来达到获取进度的目的。
七、响应进度监控的特例
上面不论是Fetch API 还是 XHR API 都不是百分百可行的。在开启服务端 nginx 的 gzip 压缩时,两种获取进度的能力将全部失效。无法在传输的过程中获取到报文的总大小或者 response 的 header 头中无 content-length 属性。
在Http 1.0及之前版本中,content-length字段可有可无。
在http1.1及之后版本,如果是 keep alive,如果存在Transfer-Encoding(重点是chunked),则在header中不能有Content-Length,有也会被忽视。 content-length 和 Transfer-Encoding:chunked 必然是二选一。若是非keep alive,则和http1.0一样。content-length可有可无。
在nginx开启gzip压缩时,则 response 的 header 头中存在的是 Transfer-Encoding:chunked,则不会存在 content-length。
所以在这种情况下,无法实现报文相应进度监控,但是能持续获取到报文的大小已传输的大小