一次搞懂:在Vue里用Showdown渲染Markdown+KaTeX数学公式

一个前端小白的踩坑日记,帮你避开那些"根号变触手"的诡异场面

事情是这样的

前两天接到一个需求:数据库里存了一堆 Markdown 格式的文本,里面还夹杂着各种数学公式,比如 $E=mc^2$ 这种,更狠的还有 $$\int_{-\infty}^{\infty} e^{-x^2} dx = \sqrt{\pi}$$。要在 Vue 页面里把它们渲染得漂漂亮亮的,公式要有专业排版,不能用图片糊弄。

我心想:Markdown 渲染嘛,网上插件一抓一大把,分分钟搞定。结果......一抓就是一把 bug,尤其是那些数学公式,跟成精了似的,根号能变出分身来。这一折腾就是两天,最后总算整明白了。今天就把这个过程揉碎了写给你,让你少走点弯路。

市面上的 Markdown 解析器很多,什么 marked、markdown-it、showdown......我最后选了 showdown,原因就两条:

  • :老牌库,这么多年了,坑基本都被填平了。
  • 插件友好showdown-katex 插件刚好能满足我们 LaTeX 公式的需求,而且配置非常灵活。

装起来也就两行命令的事儿:

bash 复制代码
npm install showdown
npm install showdown-katex

先上能跑的代码,免得你们着急。这是一个简单的 Vue 组件,用来展示带公式的 Markdown 内容。

vue 复制代码
<template>
  <div class="msg" v-html="transformMsg(msgInfo)"></div>
</template>

<script>
import showdown from "showdown";
import showdownKatex from "showdown-katex";

export default {
  data() {
    return {
      msgInfo: ""  // 从后端拿到的 Markdown 字符串
    }
  },
  methods: {
    transformMsg(msgInfo = "") {
      // 第一步:预处理脏数据(后面解释为什么)
      msgInfo = msgInfo.replaceAll("<br  />", "\n");

      // 第二步:创建 showdown 转换器,配置好插件
      let converter = new showdown.Converter({
        tables: true,           // 支持表格
        strikethrough: true,    // 支持 ~~删除线~~
        underline: true,        // 防止下划线被误解析为斜体
        extensions: [
          showdownKatex({
            throwOnError: false,   // 公式写错了也别抛异常,页面别崩
            displayMode: false,    // 默认行内模式(可以改成 true 让 $$ 变块级)
            delimiters: [
              { left: "$$", right: "$$", display: false },
              { left: "$", right: "$", display: false },
              { left: "~", right: "~", display: false, asciimath: true },
            ],
          }),
        ],
      });

      // 第三步:转成 HTML 并返回
      return converter.makeHtml(msgInfo);
    }
  }
}
</script>

看着挺简单对吧?我当时也是这么想的,直到我遇到了那个"根号怪"。

奇奇怪怪问题一:根号怎么变分身了?

第一个让我破防的 bug:只要公式里有根号(比如 $\sqrt{2}$),页面上就会出现两个根号!第一个是正常的,第二个像个伸着长脖子的怪物,根号线无限拉长,里面的公式还重复显示。那场面,简直像公式在分裂繁殖。

经过一番搜索(其实就是去 GitHub 翻 issue),才知道这是 KaTeX 渲染机制导致的。KaTeX 为了兼顾屏幕阅读器,会同时生成两个 DOM 结构:一个用于正常显示(.katex),另一个做辅助(.katex-html)。在某些复杂公式(尤其是根号)下,两个结构都会在页面上渲染出来,就造成了重影。

解决方法?简单粗暴,CSS 一刀切:

css 复制代码
.katex-html {
  display: none;
}

放心,这不会影响正常公式的显示,屏幕阅读器也能从 .katex 里读取内容。加完这句,根号立马老实了,世界清净。

奇奇怪怪问题二:数据库里的 <br /> 不听话

我们的数据是老系统倒过来的,里面换行用的是 <br />(中间俩空格)。结果 showdown 转换后,这些 <br /> 原样输出到了 HTML 里,根本没变成换行。页面上一段话全挤在一起,像没分段的作文。

解决办法是在转换前做替换,把 <br /> 变成 Markdown 认的换行符 \n

javascript 复制代码
msgInfo = msgInfo.replaceAll("<br  />", "\n");

如果你数据库里还有 <br><br/> 等各种变体,可以写个正则一把抓:

javascript 复制代码
msgInfo = msgInfo.replace(/<br\s*\/?>/gi, "\n");

奇奇怪怪问题三:下划线被当成斜体,公式乱套

物理老师最爱写 F_gravity,但 Markdown 里下划线默认表示斜体。于是 F_gravity 就变成了 F<em>gravity</em>,显示出来"F gravity"斜了,完全不是那个意思。

showdown 提供了一个 underline 选项,设为 true 后,__双下划线__ 会变成下划线,但单下划线 _ 还是斜体。这不够啊,我们想要的是:在普通文字里保留 Markdown 语法,但公式里的下划线别捣乱

其实 showdown-katex 插件在解析时会先把公式内容"保护"起来,只要你的公式被 $...$$$...$$ 包住,里面的下划线就不会被 Markdown 引擎解析。所以关键是:所有公式都必须写定界符

如果历史数据里确实有裸下划线没加 $,那只能写个脚本批量包一下,或者在转换前用正则把 \b_\w+_\b 之类的模式手动包成公式。这活儿有点糙,但能救急。

奇奇怪怪问题四:行内公式和块级公式不分家

看上面代码的 delimiters 配置,$$$display 都是 false,这意味着无论你写 $E=mc^2$ 还是 $$E=mc^2$$,都会被当成行内公式,不换行,不居中。这显然不符合常识。

正确的配置应该是:$ 行内,$$ 块级。改一下:

javascript 复制代码
delimiters: [
  { left: "$$", right: "$$", display: true },   // 块级公式,独占一行
  { left: "$", right: "$", display: false },    // 行内公式,夹在文字中间
  { left: "~", right: "~", display: false, asciimath: true }
]

当然,如果你希望 $$ 也当行内用,那就保持原样。但根据 LaTeX 习惯,还是建议区分开来。

如果直接在模板里写 v-html="transformMsg(msgInfo)",每次组件重新渲染(比如窗口滚动、数据变化)都会重新解析 Markdown 和公式。公式一多,页面就卡得像幻灯片。

改成计算属性(computed)缓存一下结果:

javascript 复制代码
computed: {
  renderedHtml() {
    return this.transformMsg(this.msgInfo);
  }
}

模板里用 v-html="renderedHtml",只有当 msgInfo 真正变化时才会重新转换。

KaTeX 自带样式,但默认块级公式的上下边距可能跟你的页面不搭。可以自己在全局 CSS 里调一下:

css 复制代码
.katex-display {
  margin: 1.5em 0;
  overflow-x: auto;  /* 防止超宽公式溢出 */
}

如果行内公式显得太小,可以来个:

css 复制代码
.katex {
  font-size: 1.05em;
}

throwOnError: false 是个好习惯。公式写错了,页面不会白屏,只会显示一段红色的错误提示。用户至少能看到"这里有个公式没渲染好",而不是整个页面崩掉。

我把上面的点整合成一个 Vue 单文件组件,你直接拷到项目里就能用:

vue 复制代码
<template>
  <div class="markdown-math" v-html="renderedHtml"></div>
</template>

<script>
import showdown from "showdown";
import showdownKatex from "showdown-katex";

export default {
  name: "MarkdownMath",
  props: {
    content: {
      type: String,
      default: ""
    },
    // 可选:自定义定界符
    delimiters: {
      type: Array,
      default: null
    }
  },
  computed: {
    renderedHtml() {
      return this.transform(this.content);
    }
  },
  methods: {
    transform(raw) {
      if (!raw) return "";
      // 预处理奇怪的换行
      let processed = raw.replace(/<br\s*\/?>/gi, "\n");

      const customDelimiters = this.delimiters || [
        { left: "$$", right: "$$", display: true },
        { left: "$", right: "$", display: false },
        { left: "\\[", right: "\\]", display: true },
        { left: "\\(", right: "\\)", display: false }
      ];

      const converter = new showdown.Converter({
        tables: true,
        strikethrough: true,
        underline: false,   // 关掉下划线解析,避免干扰公式
        simpleLineBreaks: true,
        extensions: [
          showdownKatex({
            throwOnError: false,
            delimiters: customDelimiters
          })
        ]
      });
      return converter.makeHtml(processed);
    }
  }
};
</script>

<style scoped>
/* 组件内部样式 */
.markdown-math {
  line-height: 1.6;
  word-wrap: break-word;
}
</style>

<style>
/* 全局样式,解决根号分身 */
.katex-html {
  display: none;
}
.katex-display {
  margin: 1em 0;
  overflow-x: auto;
  overflow-y: hidden;
}
</style>
相关推荐
m0_738120721 小时前
应急响应(重点)——记一次某公司流量应急溯源分析(附带下载链接)
服务器·前端·数据库·安全·web安全·网络安全
前端Hardy1 小时前
pnpm 11.0 正式登场:安装起飞、安全拉满、彻底告别 npm 依赖
前端
PILIPALAPENG1 小时前
第4周 Day 1:智能体记忆系统——给 Agent 一个"大脑"
前端·人工智能·python
_风满楼1 小时前
TDD实战-会议室冲突检测的红绿重构循环
前端·javascript·算法
Rkgua2 小时前
JS中的惰性函数基本介绍
前端·javascript
客场消音器2 小时前
我用两周半 Vibe Coding 做了一个前额叶训练的微信小程序
前端·javascript·后端
铁皮饭盒3 小时前
成为AI全栈 - 第4课:Drizzle ORM SQLite Elysia 数据库实战
前端·后端
ascarl20103 小时前
Linux.do 帖子整理:AI 调用 Chrome DevTools 调试前端页面
linux·前端·人工智能
DanCheOo3 小时前
开源 | 我是怎么用 ai-memory 让 Cursor 每次开新对话都自动知道项目背景的
前端·人工智能·ai·ai编程