什么是流式文本传输?
流式文本传输是一种在数据生成后立即传输,而不是等待所有数据生成完毕再一次性传输的技术。这种传输方式可以显著提升用户体验,特别是在处理大量数据或需要实时响应的场景中。
流式传输的主要优势:
- 实时响应:用户可以立即看到部分结果,而不必等待整个过程完成
- 减少内存占用:数据可以边生成边传输,避免在内存中缓存大量数据
- 更好的用户体验:特别是在AI生成内容、长文本处理等场景下
NodeJS + Koa 流式接口示例
javascript
const Koa = require('koa');
const { Readable } = require('stream');
const app = new Koa();
// 流式响应中间件
app.use(async (ctx) => {
// 设置响应头
ctx.set('Content-Type', 'text/plain');
ctx.set('Transfer-Encoding', 'chunked');
// 创建一个可读流
const readableStream = new Readable({
read(size) {
// 这个方法会在流需要更多数据时被调用
}
});
// 模拟异步生成数据的过程
async function generateData() {
const data = '这是,第一部分,数据,\n这是,第二部分,数据\n这是,第三部分,数据,\n这是,第四部分,数据,\n这是最后一部分数据\n'.split(',');
for (const chunk of data) {
// 模拟处理延迟
await new Promise(resolve => setTimeout(resolve, 100));
// 将数据推入流中
readableStream.push(chunk);
}
// 结束流
readableStream.push(null);
}
// 启动数据生成
generateData();
// 将流管道到响应对象
ctx.body = readableStream;
});
// 启动服务器
const PORT = 3000;
app.listen(PORT, () => {
console.log(`服务器运行在 http://localhost:${PORT}`);
});
NodeJS 发送流式请求
javascript
const https = require('https');
const http = require('http');
/**
* Node.js版本的流式查询函数
* @param {Object} options - 配置选项
* @param {Object} options.params - 请求参数
* @param {Function} [options.onStart] - 开始回调
* @param {Function} [options.onChange] - 数据变化回调
* @param {Function} [options.onFinish] - 完成回调
* @param {Function} [options.onError] - 错误回调
* @param {Function} [options.chunkTransform] - 数据块转换函数
* @returns {Object} 返回包含abort方法的对象
*/
const streamRequest = ({
params,
onStart,
onChange,
onFinish,
onError,
chunkTransform,
}) => {
const { url, method = 'GET', headers = {} } = params;
const { searchParams } = params;
const { body } = params;
const parsedUrl = new URL(url);
const isHttps = parsedUrl.protocol === 'https:';
const requestModule = isHttps ? https : http;
if (searchParams) {
Object.entries(searchParams).forEach(([key, value]) => {
parsedUrl.searchParams.set(key, value);
});
}
const requestOptions = {
hostname: parsedUrl.hostname,
port: parsedUrl.port || (isHttps ? 443 : 80),
path: parsedUrl.pathname + parsedUrl.search,
method,
headers: {
'Content-Type': 'application/json',
...headers,
},
};
let fullText = '';
let aborted = false;
const req = requestModule.request(requestOptions, (res) => {
if (res.statusCode !== 200) {
let errorData = '';
res.on('data', (chunk) => {
errorData += chunk.toString();
});
res.on('end', () => {
const error = new Error(`${res.statusMessage}\n${errorData}`);
onError?.(error);
});
return;
}
res.setEncoding('utf8');
// 监听第一个数据块
res.once('data', () => {
onStart?.();
});
res.on('data', (chunk) => {
if (aborted) return;
const chunkStr = chunk?.toString();
const transformedChunk = chunkTransform ? chunkTransform(chunkStr) : chunkStr;
fullText += transformedChunk;
onChange?.(transformedChunk, fullText);
});
res.on('end', () => {
if (!aborted) {
onFinish?.(fullText);
}
});
res.on('error', (error) => {
if (typeof onError !== 'function') {
console.error(error);
return;
}
if (!aborted) {
console.error('Response error:', error);
onError?.(error);
}
});
});
req.on('error', (error) => {
if (!aborted) {
console.error('Request error:', error);
onError?.(error);
}
});
// 发送请求体(如果是POST请求)
if (method === 'POST' && body) {
req.write(JSON.stringify(body));
}
req.end();
return {
abort: () => {
aborted = true;
req.destroy();
console.log('Request aborted');
},
};
};
浏览器中发送流式请求
- 基于
fetch实现
javascript
/**
* 浏览器版本的流式查询函数
* @param options - 配置选项
* @returns 返回包含abort方法的对象
*/
export const browserStreamRequest = ({
params,
onStart,
onChange,
onFinish,
onError,
chunkTransform,
}) => {
const { url, method = 'GET', headers = {} } = params;
const { searchParams } = params;
const { body } = params;
let controller = null;
let fullText = '';
let isStarted = false;
let isFinished = false;
const buildFetchUrl = () => {
if (!searchParams) return url;
const urlObj = new URL(url, window.location.origin);
Object.entries(searchParams).forEach(([key, value]) => {
urlObj.searchParams.set(key, value);
});
return urlObj.toString();
};
const fetchOptions = {
method,
headers: {
'Content-Type': 'application/json',
...headers,
},
// 允许跨域凭证
credentials: 'include',
};
// 设置信号用于中断请求
controller = new AbortController();
fetchOptions.signal = controller.signal;
// 添加请求体(如果是POST请求)
if (method === 'POST' && body) {
fetchOptions.body = JSON.stringify(body);
}
const processStream = async () => {
try {
const response = await fetch(buildFetchUrl(), fetchOptions);
if (!response.ok) {
let errorData = '';
try {
errorData = await response.text();
} catch (e) {
// 忽略解析错误
}
const error = new Error(`${response.status} ${response.statusText}\n${errorData}`);
onError?.(error);
return;
}
// 检查响应是否支持流式处理
if (!response.body) {
const error = new Error('Response body is not streamable');
onError?.(error);
return;
}
// 创建TextDecoder用于解码二进制流
const decoder = new TextDecoder('utf-8');
const reader = response.body.getReader();
// 流式处理数据
const readStream = async () => {
try {
const { done, value } = await reader.read();
if (done) {
if (!isFinished) {
isFinished = true;
onFinish?.(fullText);
}
return;
}
// 首次收到数据时触发onStart
if (!isStarted) {
isStarted = true;
onStart?.();
}
// 解码并处理数据块
const chunkStr = decoder.decode(value, { stream: true });
const transformedChunk = chunkTransform ? chunkTransform(chunkStr) : chunkStr;
fullText += transformedChunk;
onChange?.(transformedChunk, fullText);
// 继续读取流
readStream();
} catch (error) {
if (!isFinished && !controller?.signal.aborted) {
console.error('Stream processing error:', error);
onError?.(error instanceof Error ? error : new Error(String(error)));
}
}
};
readStream();
} catch (error) {
if (!isFinished && !controller?.signal.aborted) {
console.error('Fetch request error:', error);
onError?.(error instanceof Error ? error : new Error(String(error)));
}
}
};
// 立即开始处理流
processStream();
return {
abort: () => {
if (controller) {
controller.abort();
isFinished = true;
console.log('Request aborted');
}
},
};
};
- 低版本浏览器(如 IE11)不支持
fetch流式传输,需要使用 XHR 实现
javascript
/**
* 浏览器兼容性检查
* @returns 是否支持流式请求
*/
export const isStreamSupported = () => {
return !!(window.fetch &&
window.ReadableStream &&
window.TextDecoder &&
window.AbortController);
};
/**
* 降级版本的请求函数(当浏览器不支持流式处理时使用)
* @param options - 配置选项
* @returns 返回包含abort方法的对象
*/
export const fallbackRequest = ({
params,
onStart,
onChange,
onFinish,
onError,
chunkTransform,
}) => {
const { url, method = 'GET', headers = {} } = params;
const { searchParams } = params;
const { body } = params;
let xhr = null;
let fullText = '';
const buildUrl = () => {
if (!searchParams) return url;
const urlObj = new URL(url, window.location.origin);
Object.entries(searchParams).forEach(([key, value]) => {
urlObj.searchParams.set(key, value);
});
return urlObj.toString();
};
try {
xhr = new XMLHttpRequest();
xhr.open(method, buildUrl(), true);
// 设置响应类型为文本
xhr.responseType = 'text';
// 设置请求头
Object.entries({
'Content-Type': 'application/json',
...headers,
}).forEach(([key, value]) => {
xhr?.setRequestHeader(key, value);
});
// 监听进度事件(非标准流式处理)
xhr.onprogress = (event) => {
if (event.lengthComputable || xhr?.responseText) {
const currentText = xhr?.responseText || '';
const newChunk = currentText.substring(fullText.length);
if (newChunk && fullText.length === 0) {
// 首次收到数据
onStart?.();
}
if (newChunk) {
const transformedChunk = chunkTransform ? chunkTransform(newChunk) : newChunk;
fullText += transformedChunk;
onChange?.(transformedChunk, fullText);
}
}
};
xhr.onload = () => {
if (xhr?.status === 200) {
// 确保处理完所有数据
const currentText = xhr.responseText || '';
if (currentText !== fullText) {
const newChunk = currentText.substring(fullText.length);
const transformedChunk = chunkTransform ? chunkTransform(newChunk) : newChunk;
fullText += transformedChunk;
onChange?.(transformedChunk, fullText);
}
onFinish?.(fullText);
} else {
const error = new Error(`${xhr?.status} ${xhr?.statusText}\n${xhr?.responseText || ''}`);
onError?.(error);
}
};
xhr.onerror = () => {
const error = new Error('Network error occurred');
console.error('XHR error:', error);
onError?.(error);
};
// 发送请求
if (method === 'POST' && body) {
xhr.send(JSON.stringify(body));
} else {
xhr.send();
}
} catch (error) {
console.error('Fallback request initialization error:', error);
onError?.(error instanceof Error ? error : new Error(String(error)));
}
return {
abort: () => {
if (xhr) {
xhr.abort();
console.log('Request aborted');
}
},
};
};
/**
* 智能流式请求函数 - 根据浏览器支持情况自动选择实现方式
* @param options - 配置选项
* @returns 返回包含abort方法的对象
*/
export const smartStreamRequest = (options) => {
if (isStreamSupported()) {
console.log('Using native fetch stream implementation');
return browserStreamRequest(options);
} else {
console.log('Using fallback XHR implementation');
return fallbackRequest(options);
}
};
浏览器端流式请求使用示例
javascript
// 基本使用示例
const streamInstance = smartStreamRequest({
params: {
url: '/api/stream',
method: 'POST',
body: {
prompt: 'Tell me a story',
stream: true
}
},
onStart: () => {
console.log('Stream started');
document.getElementById('status')?.textContent('Streaming...');
},
onChange: (chunk, fullText) => {
console.log(`Received chunk: ${chunk}`);
document.getElementById('output')?.textContent(fullText);
},
onFinish: (fullText) => {
console.log(`Stream finished, total text: ${fullText.length} characters`);
document.getElementById('status')?.textContent('Completed');
},
onError: (error) => {
console.error('Stream error:', error);
document.getElementById('status')?.textContent(`Error: ${error.message}`);
},
// 可选的数据转换函数
chunkTransform: (chunk) => {
// 例如处理特定格式的流式数据
return chunk.replace(/^data: /, '');
}
});
// 中断请求的示例
setTimeout(() => {
streamInstance.abort();
}, 5000);
常见问题处理
- CORS 问题:确保服务器端正确配置了 CORS 响应头,特别是当需要流式传输时
- 超时处理 :可以通过
setTimeout和abort()方法实现请求超时控制 - 内存管理:对于长时间运行的流,注意监控内存使用情况,避免内存泄漏
- 错误恢复 :可以实现重试逻辑,在
onError回调中重新发起请求
案例: 流式调用免费的AI大模型接口(来自硅基流动)
备注:硅基流动(SiliconFlow)是一家提供免费AI大模型服务的公司,其API接口支持流式传输。其中token可以在 硅基流动API密钥管理 处创建。API介绍见:API手册
typescript
async function callSiliconFlowAPI() {
const options = {
url: 'https://api.siliconflow.cn/v1/chat/completions',
method: 'POST',
headers: { Authorization: 'Bearer <token>', 'Content-Type': 'application/json' },
body: {
model: 'Qwen/Qwen2.5-7B-Instruct',
messages: [{ role: 'user', content: '你好' }],
stream: true,
},
};
try {
streamRequest({
params: options,
onChange: (chunk, fullText) => {
process.stdout.write(chunk);
},
chunkTransform: extractContentFromSiliconFlowChunk,
});
} catch (error) {
console.error(error);
}
}
/**
* 从 SiliconFlow 模型的响应 chunk 中提取实际内容
* @param {string} chunk - 包含 SiliconFlow 响应的 chunk 字符串
* @returns {string} - 提取后的实际内容
*/
function extractContentFromSiliconFlowChunk (chunk) {
return chunk?.trim()
.split('data: ')
.filter(item => item && !item.includes('[DONE]'))
.map(item => {
const json = JSON.parse(item.trim());
const delta = json?.choices?.[0]?.message || json?.choices?.[0]?.delta;
const res = delta?.content ?? delta?.reasoning_content ?? '';
return res;
})
.filter(Boolean).join('');
}