目录
- 一、引言
- 二、核心技术解析
-
- [2.1 打字机效果(Typewriter Effect)](#2.1 打字机效果(Typewriter Effect))
- [2.2 流式输出(Streaming Output)](#2.2 流式输出(Streaming Output))
-
- [方案一:Server-Sent Events (SSE)](#方案一:Server-Sent Events (SSE))
- 方案二:WebSocket
- [方案三:HTTP/2 分块传输](#方案三:HTTP/2 分块传输)
- 三、总结
- 四、效果体验
一、引言
不知道你是否注意到,在与 ChatGPT 等 AI 对话时,回复内容是一个字一个字"蹦"出来的,这种即时反馈的体验远比等待完整响应更让人感到亲切和自然。
这种效果背后融合了两种核心技术: 前端打字机效果 和 后端流式传输。本文将从原理到实践,带你深入理解并实现这一功能。
二、核心技术解析
2.1 打字机效果(Typewriter Effect)
纯前端实现,通过 JavaScript 定时器逐字符显示文本:
javascript
// 基础实现
function typeWriter(text, elementId, speed = 50) {
const element = document.getElementById(elementId);
let index = 0;
function type() {
if (index < text.length) {
element.innerHTML += text.charAt(index);
index++;
setTimeout(type, speed);
}
}
type();
}
// 使用示例
typeWriter("Hello, World!", "output");
2.2 流式输出(Streaming Output)
服务端实时推送数据,前端渐进式接收。主流方案有三种:
方案一:Server-Sent Events (SSE)
javascript
// 前端代码
const eventSource = new EventSource('/api/stream');
eventSource.onmessage = (event) => {
document.getElementById('output').innerHTML += event.data;
};
// 后端(Node.js)
app.get('/api/stream', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
const text = "Hello, World!";
let index = 0;
const interval = setInterval(() => {
if (index < text.length) {
res.write(`data: ${text[index]}\n\n`);
index++;
} else {
clearInterval(interval);
res.end();
}
}, 50);
});
方案二:WebSocket
javascript
// 前端
const ws = new WebSocket('ws://localhost:8080');
ws.onmessage = (event) => {
document.getElementById('output').innerHTML += event.data;
};
方案三:HTTP/2 分块传输
javascript
// 后端设置 Transfer-Encoding: chunked
res.writeHead(200, {
'Transfer-Encoding': 'chunked',
'Content-Type': 'text/plain'
});
三、总结
| 技术方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 打字机效果 | 静态文本展示 | 简单易用 | 无法获取实时数据 |
| SSE | 服务端单向推送 | 轻量、自动重连 | 只支持单向通信 |
| WebSocket | 双向实时通信 | 全双工、低延迟 | 实现复杂 |
| HTTP/2 | 高性能传输 | 多路复用 | 需要 HTTP/2 支持 |
四、效果体验
打字机完整代码
javascript
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI 对话 - 打字机效果</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.chat-container {
width: 100%;
max-width: 800px;
height: 85vh;
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
overflow: hidden;
}
.chat-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px 30px;
text-align: center;
}
.chat-header h1 {
font-size: 24px;
font-weight: 600;
}
.chat-header p {
font-size: 14px;
opacity: 0.9;
margin-top: 5px;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px 30px;
display: flex;
flex-direction: column;
gap: 15px;
}
.message {
max-width: 80%;
padding: 12px 18px;
border-radius: 18px;
font-size: 15px;
line-height: 1.6;
word-wrap: break-word;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.user-message {
align-self: flex-end;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-bottom-right-radius: 4px;
}
.ai-message {
align-self: flex-start;
background: #f0f2f5;
color: #333;
border-bottom-left-radius: 4px;
}
.typing-indicator {
display: none;
align-self: flex-start;
background: #f0f2f5;
padding: 15px 20px;
border-radius: 18px;
border-bottom-left-radius: 4px;
}
.typing-indicator.active {
display: flex;
}
.typing-indicator span {
display: inline-block;
width: 8px;
height: 8px;
background: #999;
border-radius: 50%;
margin: 0 3px;
animation: typingBounce 1.4s infinite ease-in-out both;
}
.typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
.typing-indicator span:nth-child(2) { animation-delay: -0.16s; }
@keyframes typingBounce {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1); }
}
.chat-input-container {
padding: 20px 30px;
border-top: 1px solid #e0e0e0;
display: flex;
gap: 10px;
}
.chat-input {
flex: 1;
padding: 12px 20px;
border: 2px solid #e0e0e0;
border-radius: 25px;
font-size: 15px;
outline: none;
transition: border-color 0.3s;
}
.chat-input:focus {
border-color: #667eea;
}
.send-btn {
padding: 12px 30px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 25px;
font-size: 15px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.send-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
.send-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.cursor {
display: inline-block;
width: 2px;
height: 1.2em;
background: #667eea;
margin-left: 2px;
animation: blink 1s infinite;
vertical-align: text-bottom;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
</style>
</head>
<body>
<div class="chat-container">
<div class="chat-header">
<h1>AI 智能助手</h1>
<p>打字机效果演示</p>
</div>
<div class="chat-messages" id="chatMessages">
<div class="message ai-message">
你好!我是AI助手,有什么可以帮助你的吗?
</div>
</div>
<div class="typing-indicator" id="typingIndicator">
<span></span>
<span></span>
<span></span>
</div>
<div class="chat-input-container">
<input
type="text"
class="chat-input"
id="userInput"
placeholder="输入消息..."
onkeypress="handleKeyPress(event)"
>
<button class="send-btn" id="sendBtn" onclick="sendMessage()">发送</button>
</div>
</div>
<script>
// 固定的AI回复内容
const aiResponses = [
"这是一个打字机效果的演示!你可以看到我正在逐字输出这段文字。这种效果在AI对话中很常见,可以让用户感受到实时交互的体验。",
"打字机效果(Typewriter Effect)是一种经典的前端动画技术。它通过JavaScript定时器逐字符显示文本,模拟真实的打字过程。",
"实现打字机效果的核心是使用setTimeout或setInterval函数,配合字符串的charAt方法,逐个字符地添加到DOM元素中。",
"除了基础的打字机效果,还可以添加光标闪烁、打字音效、速度变化等增强体验的功能。",
"在现代Web应用中,打字机效果常与Server-Sent Events (SSE)或WebSocket结合使用,实现从服务器实时接收数据并逐字显示。"
];
let currentResponseIndex = 0;
let isTyping = false;
const chatMessages = document.getElementById('chatMessages');
const userInput = document.getElementById('userInput');
const sendBtn = document.getElementById('sendBtn');
const typingIndicator = document.getElementById('typingIndicator');
// 发送消息
function sendMessage() {
const message = userInput.value.trim();
if (!message || isTyping) return;
// 添加用户消息
addUserMessage(message);
userInput.value = '';
// 显示输入指示器
showTypingIndicator();
// 模拟延迟后开始打字机效果
setTimeout(() => {
hideTypingIndicator();
const response = aiResponses[currentResponseIndex % aiResponses.length];
currentResponseIndex++;
typeWriterEffect(response);
}, 1000);
}
// 处理回车键
function handleKeyPress(event) {
if (event.key === 'Enter') {
sendMessage();
}
}
// 添加用户消息
function addUserMessage(text) {
const messageDiv = document.createElement('div');
messageDiv.className = 'message user-message';
messageDiv.textContent = text;
chatMessages.appendChild(messageDiv);
scrollToBottom();
}
// 显示输入指示器
function showTypingIndicator() {
typingIndicator.classList.add('active');
scrollToBottom();
}
// 隐藏输入指示器
function hideTypingIndicator() {
typingIndicator.classList.remove('active');
}
// 打字机效果
function typeWriterEffect(text) {
isTyping = true;
sendBtn.disabled = true;
const messageDiv = document.createElement('div');
messageDiv.className = 'message ai-message';
chatMessages.appendChild(messageDiv);
const cursor = document.createElement('span');
cursor.className = 'cursor';
let index = 0;
const speed = 50; // 打字速度(毫秒)
function type() {
if (index < text.length) {
messageDiv.textContent = text.substring(0, index + 1);
messageDiv.appendChild(cursor);
index++;
scrollToBottom();
setTimeout(type, speed + Math.random() * 30); // 添加随机延迟,更自然
} else {
// 打字完成,移除光标
cursor.remove();
isTyping = false;
sendBtn.disabled = false;
}
}
type();
}
// 滚动到底部
function scrollToBottom() {
chatMessages.scrollTop = chatMessages.scrollHeight;
}
</script>
</body>
</html>