优化篇之AI Chat响应换行渲染效果不生效

问题背景

ai对话框页面的实现在进行ai回复消息的时候,你会发现没有换行情况,问题的描述是让其生成符合要求的的json文件,但是ai返回的内容页面上展示为没有换行:

image-20260217095716157

我点击复制,得到的内容如下:

{
   "title": "",
   "tags": "Milvus,Docker Compose,向量数据库,Attu可视化,镜像加速",
   "categories": "数据库,容器化,AI工具",
   "articleSummary": "保姆级教程教你用 Docker Compose 一键部署 Milvus 2.5.9 向量数据库,含镜像加速、密码配置、Attu 可视化工具安装与 Web UI 访问,全程命令复制即用,轻松搭建高性能 RAG 存储后端。"
}

实际又是有换行的,这个到底是什么问题?

问题分析

我们的chat接口是事件机制响应内容的,本次chat的事件返回内容如下:

  data:{"content":"{\n","event":"resp","isFinish":false,"type":"text"}
  data:{"content":" ","event":"resp","isFinish":false,"type":"text"}
  data:{"content":" \"","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"title","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"\":","event":"resp","isFinish":false,"type":"text"}
  data:{"content":" \"\",\n","event":"resp","isFinish":false,"type":"text"}
  data:{"content":" ","event":"resp","isFinish":false,"type":"text"}
  data:{"content":" \"","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"tags","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"\":","event":"resp","isFinish":false,"type":"text"}
  data:{"content":" \"","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"Mil","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"vus","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"向量","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"数据库","event":"resp","isFinish":false,"type":"text"}
  data:{"content":",D","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"ocker","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"安装","event":"resp","isFinish":false,"type":"text"}
  data:{"content":",","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"Att","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"u","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"可视化","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"工具","event":"resp","isFinish":false,"type":"text"}
  data:{"content":",","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"镜像","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"加速","event":"resp","isFinish":false,"type":"text"}
  data:{"content":",D","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"ocker","event":"resp","isFinish":false,"type":"text"}
  data:{"content":" Compose","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"\",\n","event":"resp","isFinish":false,"type":"text"}
  data:{"content":" ","event":"resp","isFinish":false,"type":"text"}
  data:{"content":" \"","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"categories","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"\":","event":"resp","isFinish":false,"type":"text"}
  data:{"content":" \"","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"向量","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"数据库","event":"resp","isFinish":false,"type":"text"}
  data:{"content":",D","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"ocker","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"容器","event":"resp","isFinish":false,"type":"text"}
  data:{"content":",","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"AI","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"工具","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"\",\n","event":"resp","isFinish":false,"type":"text"}
  data:{"content":" ","event":"resp","isFinish":false,"type":"text"}
  data:{"content":" \"","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"article","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"Summary","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"\":","event":"resp","isFinish":false,"type":"text"}
  data:{"content":" \"","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"本文","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"手把手","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"教你","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"用","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"Docker","event":"resp","isFinish":false,"type":"text"}
  data:{"content":" Compose","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"在","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"Linux","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"/Mac","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"上","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"极速","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"部署","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"Mil","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"vus","event":"resp","isFinish":false,"type":"text"}
  data:{"content":" ","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"2","event":"resp","isFinish":false,"type":"text"}
  data:{"content":".","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"5","event":"resp","isFinish":false,"type":"text"}
  data:{"content":".","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"9","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"向量","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"数据库","event":"resp","isFinish":false,"type":"text"}
  data:{"content":",","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"含","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"镜像","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"加速","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"、","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"密码","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"配置","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"、","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"Att","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"u","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"可视化","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"连接","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"及","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"常见","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"坑","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"解决","event":"resp","isFinish":false,"type":"text"}
  data:{"content":",","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"一站式","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"搞定","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"生产","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"级","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"R","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"AG","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"底座","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"。\"\n","event":"resp","isFinish":false,"type":"text"}
  data:{"content":"}","event":"resp","isFinish":false,"type":"text"}
  data:{"event":"resp","isFinish":true,"type":"text"}

你会发现其实其中是有\n的,那为什么页面上的展示没有展示换行效果呢?

对于页面上内容为一个message框,这个实际上是使用的markdown渲染器来实现的可视化:

image-20260217100155123

本质问题原因为markdown渲染情况下,如果是一个\n 是不会显示出换行来的,对于本地的typora、语雀之类的,实际应该是做了兼容,理论需要两个\n\n即可实现换行。

解决思路实现

解决方式

在进行new MarkdownIt实例的时候设置参数即可:

image-20260225005910108

  • breaks: true 已添加到 MarkdownIt 配置中,使得单个换行符(\n)就会渲染为 <br> 换行,不再需要空行。
  • 其余逻辑(代码高亮、复制按钮、深色模式兼容等)保持不变。

原理介绍

在标准的 Markdown 语法(如 CommonMark 规范)中,换行规则是:

  • 两个空格 + 换行 = 软换行(渲染为 <br>)
  • 两个换行(一个空行) = 段落结束(渲染为 </p><p>)

这就是为什么默认情况下,你写:

第一行
第二行

会渲染成:第一行 第二行(在同一行)

breaks: true 做了什么?

当你在 MarkdownIt 配置中设置 breaks: true 时,它修改了 Markdown 解析器的换行处理规则:

默认行为:

第一行\n第二行\n\n第三行

↓ 解析为

<p>第一行 第二行</p>
<p>第三行</p>

开启 breaks: true 后:

第一行\n第二行\n\n第三行

↓ 解析为

<p>第一行<br>第二行</p>
<p>第三行</p>

测试效果

普通文本情况下:

image-20260225010119453

代码撰写情况下:

image-20260225010139334

没有问题!


整理者:长路 时间:2026.2.17

评论区请在客户端页面查看