前端实现流式输出配合katex.js

近期在业务中有做AI小模型实现定制化与客制化开发,要求实现流式输出复杂数学公式与数学符号,前端目前较为好用的公式库就是katex.js

一、katex.js

安装 KaTeX 依赖

在 Vue 项目中安装 katex 及其样式库:

bash 复制代码
npm install katex
npm install katex/dist/katex.min.css

全局引入 KaTeX 样式

在项目的入口文件(如 main.js)中引入 KaTeX 的 CSS:

javascript 复制代码
import 'katex/dist/katex.min.css'

在组件中使用 KaTeX

通过 import 引入 KaTeX 的核心方法,并在 Vue 组件的模板或方法中调用:

javascript 复制代码
import { renderToString } from 'katex'

在模板中动态渲染公式时,可以使用 v-html 绑定 KaTeX 生成的 HTML:

html 复制代码
<template>
  <div v-html="latexFormula"></div>
</template>

<script>
export default {
  data() {
    return {
      formula: 'c = \\pm\\sqrt{a^2 + b^2}'
    }
  },
  computed: {
    latexFormula() {
      return renderToString(this.formula, {
        throwOnError: false // 忽略公式错误
      })
    }
  }
}
</script>

按需加载 KaTeX 组件

将Katex封装成一个通用工具函数以方便在组件中引用和处理内容

javascript 复制代码
import {
	marked
} from "marked";
import hljs from "highlight.js";
import katex from "katex";
// 必须保留 katex 样式,否则公式无样式
import "katex/dist/katex.min.css";
import "highlight.js/styles/github.css";
import "github-markdown-css";
marked.use({
	extensions: [{
			name: "mathBlock",
			level: "block",
			start(src) {
				return src.indexOf("$$");
			},
			tokenizer(src, tokens) {
				const match = src.match(/^\$\$([\s\S]+?)\$\$/);
				if (match) {
					return {
						type: "mathBlock",
						raw: match[0],
						text: match[1].trim(),
					};
				}
			},
			renderer(token) {
				return katex.renderToString(token.text, {
					displayMode: true,
					throwOnError: false,
					strict: "ignore", // 比 false 更宽松,忽略语法警告
					trust: true, // 信任复杂 LaTeX 命令
				});
			},
		},
		{
			name: "math",
			level: "inline",
			start(src) {
				return src.indexOf("$");
			},
			tokenizer(src, tokens) {
				// 优化正则:支持公式内包含 []、^、\ 等特殊字符,且避免与块级 $$ 冲突
				// 匹配规则:单个 $ 开头 → 任意字符(包括 \、[]、^)→ 单个 $ 结尾(非 $$)
				const match = src.replaceAll("<br  />", "\n").match(/^(?!\$\$)\$([\s\S]*?)(?<!\$)\$(?!\$)/);
				if (match && match[1].trim()) { // 确保公式内容非空
					return {
						type: "math",
						raw: match[0],
						text: match[1].trim(),
					};
				}
			},
			renderer(token) {
				// 核心:配置 katex 支持复杂 LaTeX 语法(\mathrm、上标、括号等)
				return katex.renderToString(token.text, {
					displayMode: false,
					throwOnError: false, // 错误不崩溃
					strict: "ignore", // 忽略非致命语法错误
					trust: true, // 允许使用 \mathrm 等命令
					macros: {
						// 可选:预定义常用宏,确保 \mathrm 生效
						"\\mathrm": "\\text{#1}",
					},
				});
			},
		},
	],
});
// 配置 marked
const renderer = new marked.Renderer();

// 配置 marked
marked.setOptions({
	renderer: renderer,
	highlight: function(code, lang) {
		if (lang && hljs.getLanguage(lang)) {
			return hljs.highlight(code, {
				language: lang
			}).value;
		}
		return hljs.highlightAuto(code).value;
	},
	breaks: false, // 将换行符转换为 <br>
	tables: true, // 启用表格支持
	gfm: true, // 启用 GitHub 风格的 Markdown
	pedantic: false, // 不要过分严格
	sanitize: false, // 不要净化 HTML
	smartLists: true, // 使用更智能的列表行为
	smartypants: false, // 使用更智能的标点符号
	xhtml: false // 不要闭合空标签
});



export default marked;

安全性注意事项

使用 v-html 时,确保公式内容可信或经过过滤,避免 XSS 攻击。KaTeX 默认会过滤危险标签,但混合用户输入时仍需谨慎。

二、流式输出

流式输出的基本概念

流式输出(Streaming)指数据分块传输并实时渲染到页面的技术,适用于聊天应用、日志监控等需要动态更新内容的场景。前端实现的核心是通过分块接收数据并逐步渲染,避免等待全部数据加载完成。

使用 Fetch API 处理流式数据

Fetch API 的 response.body 返回一个 ReadableStream 对象,可通过 getReader() 逐块读取数据:

javascript 复制代码
fetch('/api/stream')
  .then(response => {
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    function readChunk() {
      return reader.read().then(({ done, value }) => {
        if (done) return;
        const text = decoder.decode(value);
        document.getElementById('output').innerHTML += text;
        return readChunk(); // 递归读取下一块
      });
    }
    return readChunk();
  });

使用 EventSource 接收服务器推送

服务器通过 text/event-stream 格式推送数据,前端通过 EventSource 监听事件:

javascript 复制代码
const eventSource = new EventSource('/api/sse');
eventSource.onmessage = (event) => {
  document.getElementById('output').innerHTML += event.data;
};

WebSocket 实时双向通信

WebSocket 适合需要双向通信的场景,如聊天室:

javascript 复制代码
const socket = new WebSocket('wss://example.com/stream');
socket.onmessage = (event) => {
  document.getElementById('output').innerHTML += event.data;
};

优化渲染性能

  • 虚拟滚动:仅渲染可视区域内容,减少 DOM 操作。
  • 防抖/节流:合并高频更新,避免界面卡顿。
  • 增量 DOM 更新:通过 Diff 算法(如 React 的 Reconciliation)最小化渲染开销。

注意事项

  • 编码一致性:确保前后端使用相同的字符编码(如 UTF-8)。
  • 错误处理 :监听 error 事件并重连流式连接。
  • 内存管理:长时间运行的流需定期清理已渲染的数据,防止内存泄漏。

以上方法可根据具体场景组合使用,例如 Fetch API + 虚拟滚动实现高效日志展示。

完整函数示例

javascript 复制代码
        /**
         * 流式请求处理函数
         */
        async streamRequest(url, data = "") {
            try {
                try {
                    const response = await fetch(baseUrl + url, {
                        method: "post",
                        headers: { Accept: "application/json", "content-type": "application/json" },
                        body: JSON.stringify({
                            session_id: this.historyInfo.session_id || "",
                            query: data,
                        })
                    });
                    if (!response.body) {
                        throw new Error("ReadableStream not supported in this browser.");
                    }
                    const reader = response.body.getReader();
                    const decoder = new TextDecoder("utf-8");
                    let { value, done } = await reader.read();
                    // 直接处理响应文本
                    const assistantMsg = {
                        id: this.messages.length,
                        role: "assistant",
                        raw: "",
                        data: "",
                        needThinking: false,
                        dialog_type: "other",
                        endLog: false,
                        steps: [],
                        type: "",
                        needFile: false,
                        files: [],
                    };
                    this.currentTypingText = "";
                    this.messages.push(assistantMsg);

                    while (!done) {
                        const text = decoder.decode(value, { stream: true });
                        assistantMsg.raw += text.slice(6).split("||")[0];
                        this.$set(assistantMsg, "data", assistantMsg.raw);
                        if (data === "[DONE]") {
                            break;
                        }
                        ({ value, done } = await reader.read());

                    }
                    this.scrollToBottom();
                } catch (error) {
                    console.error("Error fetching stream data:", error);
                }
            } catch (error) {
                console.error("Stream request error:", error);
                // this.$message.error("请求失败,请重试");
                return null;
            }
        },
相关推荐
小宇的天下2 小时前
Calibre :Standard Verification Rule Format(SVRF) Manual (1-1)
大数据·前端·网络
滴水未满2 小时前
uniapp的页面
前端·uni-app
无限进步_2 小时前
二叉搜索树(BST)详解:从原理到实现
开发语言·数据结构·c++·ide·后端·github·visual studio
邝邝邝邝丹2 小时前
vue2-computed、JS事件循环、try/catch、响应式依赖追踪知识点整理
开发语言·前端·javascript
郝学胜-神的一滴2 小时前
机器学习特征选择:深入理解移除低方差特征与sklearn的VarianceThreshold
开发语言·人工智能·python·机器学习·概率论·sklearn
多多*2 小时前
计算机网络相关 讲一下rpc与传统http的区别
java·开发语言·网络·jvm·c#
码农水水2 小时前
阿里Java面试被问:Online DDL的INSTANT、INPLACE、COPY算法差异
java·服务器·前端·数据库·mysql·算法·面试
Coder_Boy_2 小时前
基于SpringAI的在线考试系统-知识点管理与试题管理模块联合回归测试文档
前端·人工智能·spring boot·架构·领域驱动
小旭95272 小时前
【Java 基础】IO 流 全面详解
java·开发语言