如何实现代码类似AI在页面上“打字式”输出的效果

前端 潘老师 3个月前 (02-06) 57 ℃ (0) 扫码查看

在使用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-classhljs-functionhljs-title等类名变成了hljs-stringhljs-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>
  );
}

这种方法虽然牺牲了一些性能,但目前我还没想到更好的替代方案。要是大家有更优的实现方式,欢迎分享交流!


版权声明:本站文章,如无说明,均为本站原创,转载请注明文章来源。如有侵权,请联系博主删除。
本文链接:https://www.panziye.com/front/13955.html
喜欢 (0)
请潘老师喝杯Coffee吧!】
分享 (0)
用户头像
发表我的评论
取消评论
表情 贴图 签到 代码

Hi,您需要填写昵称和邮箱!

  • 昵称【必填】
  • 邮箱【必填】
  • 网址【可选】