前端技术之---打字机效果与流式输出

目录

一、引言

不知道你是否注意到,在与 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>
相关推荐
Mintopia1 小时前
Tanstack为什么会火
前端
DongWook1 小时前
关于Harness Engineering的一次实践
前端·后端
风骏时光牛马1 小时前
Kotlin开发高频疑难问题汇总梳理
前端
暗冰ཏོ1 小时前
ECharts 前端图表开发全攻略:参数配置、项目实战与高级可视化资源整理
前端·vue.js·echarts·visual studio code
PILIPALAPENG1 小时前
gh:终端里的GitHub总控台,AI时代的开发者神器
前端·人工智能·后端
用户059540174461 小时前
Redis持久化踩坑实录:RDB+AOF混合持久化,竟会悄无声息丢数据?我用pytest+Docker复现了30次故障场景
前端·css
浮游本尊1 小时前
项目全景 + 第一条完整后端链路
java·前端
小新1101 小时前
vue架的网站修改端口
前端·javascript·vue.js
暗不需求1 小时前
从零实现一个 Vue Todos 任务清单:深入响应式编程与组合式 API
前端·vue.js·面试