通过fetch,如果要获取流式数据可以如下处理:
async function getRes(content) {
const res = await fetch(url, {...});
const reader = res.body.getReader();
// 读取数据流的第一块数据,done表示数据流是否完成,value表示当前的数
const {done, value} = await reader.read();
// 上面读取到的是数据的字节码,还需要处理字节码为文本
const decoder = new TextDecoder();
const text = decoder.decode(value);
// 打印第一块的文本内容
console.log(text, done);
以上代码指执行了一块数据,还要通过循环获取剩下流式内容:
async function getRes(content) {
const res = await fetch(url, {...});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let content = '';
let isThinking = true
while(isThinking) {
// 读取数据流的第一块数据,done表示数据流是否完成,value表示当前的数
const {done, value} = await reader.read();
if (done) {
isThinking = false
break;
const text = decoder.decode(value);
// 拼接文本内容
content = content + text
console.log(content, done);
以上可以实现基本的流式输出
react结合fetch整体可以这么实现
const [history, setHistory] = useState<{ speaker: string; text: string }[]>([
{ speaker: 'bot', text: '我是你的AI助手,有什么问题都可以问我' },
const [isThinking, setIsThinking] = useState(false);
const [inputText, setInputText] = useState('');
const [abortController, setAbortController] = useState<AbortController | null>(null);
const handleSubmit = async (question?: string) => {
if(inputText==='' && question===undefined){
return
const controller = new AbortController();
setAbortController(controller);
const newHistory = [
...history,
{ speaker: 'user', text: question ? question : inputText },
{ speaker: 'bot', text: '' },
setHistory(newHistory);
setIsThinking(true);
let toBody = {
'model': 'gpt-3.5-turbo',
temperature: 0.1,
stream: true,
messages: [
role: 'user',
content: question ? question : inputText ,
setInputText('');
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + localStorage.getItem('access_token'),
body: JSON.stringify(toBody),
signal: controller.signal,
.then(async (response) => {
if (response.body!.locked) {
console.log('流已经被一个读取器锁定。');
return;
if (!response.body) throw Error('No response from server');
const reader = response.body!.getReader();
const textDecoder = new TextDecoder();
let result = true;
while (result) {
const { done, value } = await reader.read();
if (done) {
console.log('Stream ended');
result = false;
setIsThinking(false);
break;
const chunkText = textDecoder.decode(value);
let list = chunkText.match(/data: (.+)/g);
console.log('--list--', list);
if (list && list.length > 0) {
list.forEach((element) => {
let data = element.substring(6);
data === '[DONE]' ||
typeof JSON.parse(data) !== 'object' ||
(typeof JSON.parse(data) === 'object' &&
(JSON.parse(data).choices.length === 0 ||
JSON.parse(data).choices[0].delta === undefined))
if (data === '[DONE]') {
setIsThinking(false);
return;
let content = JSON.parse(data).choices[0].delta.content;
if (content) {
setHistory((history) => {
let newHistory;
// 如果聊天记录最后一条不是机器人,则拼接一条机器人回答对象
if (history[history.length - 1].speaker !== 'bot') {
newHistory = [...history, { speaker: 'bot', text: content }];
} else {
// 聊天记录最后一条是机器人,则直接在机器人回答的内容后面拼接新回答
history[history.length - 1].text = history[history.length - 1].text + content;
// 不能直接history赋值,要加上[... ]生成新对象,否则setState会认为引用地址没变,不执行页面刷新
newHistory = [...history];
return newHistory;
console.log('Received chunk:', chunkText);
.catch((error) => {
setIsThinking(false);
console.error('Error:', error);
if (error instanceof DOMException && error.name === 'AbortError') {
console.error('AbortError:', error);
//这里可以更新state,比如更新history
// 输入问题回车调用gpt
const handleKeyPress = (event: any) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
handleSubmit();
// 停止调用
const handleStop = () => {
setIsThinking(false);
if (abortController) {
try {
abortController.abort({
type: 'USER_ABORT_ACTION',
msg: '用户终止了操作',
} catch (e) {
console.log(e);
<div className={styles.chatBox}>
{history.map((item, index) => (
key={index}
className={item.speaker === 'user' ? styles.chatRight : styles.chatLeft}
{item.speaker === 'bot' && (
<div style={{ marginBottom: '10px' }}>
src={require('@/assets/images/ai.png')}
className={styles.chatLogo}
{item.text || !isThinking || index !== history.length - 1 ? (
<span style={{ display: 'block' }}>{item.text}</span>
) : (
<Spin />
{showPrompts && (
<div className={styles.promptsWrapper}>
{prompts.map((prompt, index) => (
key={index}
className={styles.promptBox}
onClick={() => handleSubmit(prompt.question)}
<div>{prompt.title}</div>
<span>{prompt.subtitle}</span>
{isThinking ? (
<div className={styles.stopWrapper} onClick={handleStop}>
<img src={require('@/assets/images/stop.png')} />
<span style={{ marginLeft: '10px' }}>
</span>
) : (
<div style={{width: '80%', bottom: '10px', position: 'fixed' }}>
<div style={{ display: 'flex' }}>
<TextArea
rows={1}
value={inputText}
onChange={handleInputChange}
onKeyPress={handleKeyPress}
src={require('@/assets/images/send.png')}
className={styles.chatSend}
onClick={() => handleSubmit()}
聊天框样式实现
.chatBox {
flex: 1;
height: 100%;
.chatRight {
display: flex;
justify-content: right;
margin: 10px 0;
margin-left: 20%;
> span {
padding: 15px;
line-height: 20px;
background-color: #4eccc0;
border-radius: 10px;
.chatLeft {
display: flex;
flex-flow: column;
align-items: flex-start;
margin-right: 20%;
> span {
padding: 15px;
background-color: #41454F;
border-radius: 10px;
line-height: 20px;
.chatWrapper {
display: flex;
flex-flow: column;
height: 100%;
overflow-y: scroll;
scrollbar-width: thin;
scrollbar-color: rgba(136, 136, 136, 0.3) transparent;
color: #fff;
margin-bottom: 30px;
.chatSend {
align-self: center;
height: 20px;
padding-left: 5px;
cursor: pointer;
.chatLogo {
height: 20px;
padding-right: 5px;
.promptsWrapper {
display: flex;
flex-flow: column;
align-items: flex-start;
.promptBox{
background: #000000;
padding: 10px;
border-radius: 10px;
margin-bottom: 5px;
margin-bottom: 5px;
span {
color: #8a93a3;
font-size: 13px;
.stopWrapper {
text-align: center;
img {
width: 20px;
bottom: 10px;
position: fixed;
最近有个云栖大会的demo展示需求,要实现个类似的打字效果,所以我找了找相关的库。找到一个还不错的~叫iTyped.js。但是最终的效果和我想要的不太一样,会有回删效果,看了下源码,所以就自己写了一个~
再夸奖下 iTyped.js 只有3K,非常小而美,完全靠JS实现的效果!
边播放语音,边出现文字的打字效果,gif 效果如下~
React 是一个基于组件化的 JavaScript 库,它通过虚拟 DOM 的方式实现高效的页面渲染。React 的实现原理可以分为以下几个步骤:
1. 初始化阶段:React 通过调用 ReactDOM.render() 方法将组件渲染到页面上,并创建虚拟 DOM 树。
2. 更新阶段:当组件的状态发生变化时,React 会重新渲染组件,并生成新的虚拟 DOM 树。
3. 对比阶段:React 会对比新旧虚拟 DOM 树的差异,找出需要更新的部分。
4. 渲染阶段:React 会将需要更新的部分重新渲染到页面上。
React 的实现原理主要依赖于虚拟 DOM 和组件化思想,通过将页面抽象成组件的方式,实现了高效的页面渲染和更新。