如何实现代码类似AI在页面上“打字式”输出的效果
在使用AI问答大模型时,AI都是一个字一个字的输出,那么到底该如何实现代码类似“打字式”输出的效果呢?在之前负责自研产品介绍任务时,落地页有个需求:需要以代码形式展示安装方法和核心概念,要让代码一行行、字符一个个地呈现出来,就像大模型实时作答那样。当时我做得比较仓促,后来在官网看到自己之前写的内容,就想着能不能优化得更好些。
这次展示选用的代码是一段Python示例(这是我从网上随机找的,主要用于演示,不涉及实际运行可行性)。代码如下:
class SimpleBook: def __init__(self, title, author): self.title = title self.author = author def __str__(self): return f"'{self.title}' by {self.author}" class MiniLibrary: def __init__(self): self.collection = [] def add_book(self, book): self.collection.append(book) print(f"Added: {book}") # 创建书籍和小型图书馆实例 book1 = SimpleBook("奇幻旅程", "王小小") my_library = MiniLibrary() # 添加书籍到图书馆 my_library.add_book(book1) # 打印特定书籍信息 print(my_library.collection[0])
起初,我考虑用<span>
标签包裹每一行代码,还打算给变量和函数设置不同的行类样式,现在看来这个方法有点笨拙。后来发现highlight.js
库能更便捷地实现代码展示功能。由于我使用的是React框架,所以直接下载了react-highlight
,它已经将highlight.js
作为依赖。通过npm i react-highlight -S
完成安装后,就可以进行初步尝试了。而且,还能根据喜好选择代码风格,我选用了monokai
风格。实现代码展示的代码如下:
import Highlight from 'react-highlight'; import 'highlight.js/styles/monokai.css'; import './index.scss'; const pythonCodeExample = ` class SimpleBook: def __init__(self, title, author): self.title = title self.author = author def __str__(self): return f"'{self.title}' by {self.author}" class MiniLibrary: def __init__(self): self.collection = [] def add_book(self, book): self.collection.append(book) print(f"Added: {book}") # 创建书籍和小型图书馆实例 book1 = SimpleBook("奇幻旅程", "王小小") my_library = MiniLibrary() # 添加书籍到图书馆 my_library.add_book(book1) # 打印特定书籍信息 print(my_library.collection[0]) `; export default function CodeShow() { return ( <div className="code-stage"> <Highlight className='python-code' language="python"> {pythonCodeExample} </Highlight> </div> ); }
实现代码展示后,下一步就是添加打字效果。我的思路是把pythonCodeExample
作为原始数据,再创建一个响应式变量typedCode
,利用定时器定时更新typedCode
的值。具体代码如下:
export default function CodeShow() { const [typedCode, setTypedCode] = useState(''); const indexReference = useRef(0); useEffect(() => { const intervalId = setInterval(() => { if (indexReference.current < pythonCodeExample.length) { setTypedCode((prevTypedCode) => prevTypedCode + pythonCodeExample[indexReference.current]); indexReference.current++; } else { clearInterval(intervalId); } }, 50); return () => clearInterval(intervalId); }, []); return ( <div className="code-stage"> <Highlight className='python-code' language="python"> {typedCode} </Highlight> </div> ); }
虽然实现了打字效果,但仔细查看后发现代码风格丢失了。检查发现import 'highlight.js/styles/monokai.css';
文件是存在的,进一步检查元素才知道,原来是标签类名发生了变化,从原本的hljs-class
、hljs-function
、hljs-title
等类名变成了hljs-string
、hljs-attibute
等。这意味着Highlight
组件没有正确解析切割后的代码,导致未能达到预期效果。经过一番搜索,找到了两种解决方案:
- 第一种是在打字过程中,使用
<pre>
标签显示普通文本,打字完成后再用Highlight
组件渲染高亮代码。不过这种方法存在弊端,<pre>
标签展示效果不够美观,和最终期望的样式差异较大,后期调整起来比较麻烦; - 第二种方法是让
Highlight
组件重新渲染,通过设置key={typedCode.length}
,强制Highlight
组件在每次typedCode
更新时重新解析代码,以此实现动态高亮效果。
权衡之下,我选择了第二种方法,下面是完整代码:
export default function CodeShow() { const [typedCode, setTypedCode] = useState(''); const indexReference = useRef(0); useEffect(() => { const intervalId = setInterval(() => { if (indexReference.current < pythonCodeExample.length) { setTypedCode((prevTypedCode) => prevTypedCode + pythonCodeExample[indexReference.current]); indexReference.current++; } else { clearInterval(intervalId); } }, 50); return () => clearInterval(intervalId); }, []); return ( <div className="code-stage"> <Highlight key={typedCode.length} className='python-code' language="python"> {typedCode} </Highlight> </div> ); }
这种方法虽然牺牲了一些性能,但目前我还没想到更好的替代方案。要是大家有更优的实现方式,欢迎分享交流!