一次搞懂:在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>
相关推荐
jay神3 分钟前
基于 YOLOv8 的森林火灾检测系统(Web 版)
前端·yolo
code2cat8 分钟前
HTML基础篇之第一张网页:从index.html到浏览器里的任务清单
前端·html
miss1 小时前
AI 改完不敢上线?OpenSpec 规约驱动开发实战
前端·代码规范·前端工程化
AiMan2 小时前
腾讯ima还是不可以导出左侧所有对话,只能单条导出,方法不对吗
前端
去伪存真2 小时前
并发量一高就装死?Agent 全链路“防猝死”手册
前端·agent
Moment2 小时前
AI 编程时代,聊聊我的 Mac mini 双屏桌面
前端·后端
AiMan2 小时前
先确认能不能一次把Microsofat Copilot所有内容打包一个文件导出
前端
总要学点什么2 小时前
第97篇:三维数据加载-城市建筑白模自定义着色器——让一条光带沿高度扫上去,快慢却由帧率说了算
前端·gis·webgl·cesium·着色器·空间分析·白模
上海懒图科技2 小时前
工厂厂区电子地图定位导航:厂区人车精准定位,安全合规双保障
前端·arcgis·vue
西安小哥3 小时前
端侧跑大模型,真正的门槛不在参数量:一次 596 秒到 10 秒的压缩路径复盘
前端·人工智能