基于vue渲染Latex数学公式(simplemde-editor)

9,372 阅读4分钟

网络关于vue + mathjax的中文教程和博客都弱爆了,基本都是2年前MathJax 2.7.5版本的使用反复在被炒冷饭,甚至在2020年2月份的文章中还有人在引用。许久不写前端文章的我忍不出山了。

说句题外话,在我当年想要入门前端的时候,互联网上的学习总结一抓一大把,各种项目坑都可以找到,而且还都有模有样。当我现在想要入门深度学习算法的时候,却发现学习资源,尤其是优质的中文资源就没那么好找了。曾经跟朋友聊过这个话题,当时朋友开玩笑说,前端工程师都太闲了,日常解决个大bug都可以去写总结篇文章。在掘金其实也可以发现这一点,前端社区非常活跃,深度学习领域的板块就显得相对冷清。

活跃当然是好事,只不过还是希望能够有更多真正原创内容的产出,从而能帮到更多的人吧。

本文从实际项目出发,从一开始问题都不知道怎么定义,基本一窍不通,到现在基本解决了实际中遇到的问题,梳理了整个开发思路。

本文提到的Mathjax版本是3.0.0

项目需求

需求很简单,需要让我们的markdown编辑器支持数学公式latex的写法,在发表后公式也可以正常显示。

例如,把?x = {-b \pm \sqrt{b^2-4ac} \over 2a}.?给识别成公式

x = {-b \pm \sqrt{b^2-4ac} \over 2a}.

(掘金编辑器也支持了latex~,点赞)

寻找开发思路,锁定目标

在google上初步的筛查,锁定了两个插件:katexMathJax。他们都是渲染公式的工具,看demo是都可以做到输入latex,输出html,展示为数学公式。

mathJax是公式渲染的鼻祖了,历史悠久,但依然在持续更新。不过好多跳转链接都失效了

而katex后来者居上,star更多,官网更美观,而且也展示了katex和mathjax的渲染速度对比

所以一开始我就选择了katex进行开发。

开发探索阶段

1. katex与simplemde的纠缠

我目前使用的编辑器是simplemdekatex的英文官网给出的实例写法是这样

var html = katex.renderToString("c = \\pm\\sqrt{a^2 + b^2}", {
    throwOnError: false
});

非常简洁,也没有更多的demo。我的思路是,把markdown中的公式找出来,用这个方法转化一下再加到正常的渲染结果里去,应该就可以了吧?

simplemde官网给出了一个方法previewRender,可以用于在预览生效前自定义一些渲染效果。

这样一来,问题就变成了,把公式从markdown中找出来,再把公式塞回html里去

我主要是想通过正则匹配去做这件事情,查资料查了不少时间,就是匹配不出来,更别说塞回去了(我讨厌正则)。

中间去吃了个晚饭,中断了一下前进的脚步,回来后鬼使神差打开了MathJax的官网...

这一看,就发现了神奇的大陆,在demo中,mathJax只要引入全局资源和全局配置,就能自动把全站的公式都识别出来,这样不管是在预览区还是发表之后,就一步到位全部识别了。果然姜还是老的辣。

2. 转而探索MathJax

深入地看了MathJax的Gihub官方文档之后,了解到mathJax其实是有两种识别公式的方式。

  1. 第一种就是我刚刚说的全局配置,配置好之后,脚本会在网页中寻找符合条件的公式符号(比如被包裹在$中的内容),并把它编译。

点击查看demo效果以及对应html代码

  1. 第二种是将用户在表单中输入的公式,用事件触发的方式,比如点击按钮,再去编译输入框中的内容,返回html(如知乎的形式)

点击查看demo效果以及对应html代码

尤其第一种,看上去非常简单省心。

引入资源(我为了防止国外镜像不稳定,把资源下载到了本地),全局配置,几分钟就搞定了。然后打开页面。emmm...没效果。由于太简单了,甚至都不知道问题出在哪里。。

写了个静态页面,同样的配方,是有效果的,看来问题是出在vue上啊。初步猜测,是因为vue的数据都是渲染上去的,所以MathJax脚本加载的时候,页面还没有内容。等内容有了之后,脚本早执行完了,所以导致没有效果。不过也是瞎猜的,google了其他人有没有遇到同样的问题,就遇到了开头我所说的,网上蜜汁全是2.7.5版本的实现方式,有一个window.MathJax.Hub.Queue(["Typeset", MathJax.Hub])方法好像是实现了“重载脚本”的效果。然而,我现在3.0.0版本,这个方法已经undefined了....

再次陷入僵局。

期间尝试过很多其他思路,当然现在看来都是在走弯路了。

  1. 想在编辑框加一个按钮,退而求其次,用输入框的方式去生成公式。但是公式编译后的html又长又臭,就这样插入markdown显然不好。
  2. 尝试刚刚提到的mathJax的第二种方法,把vue渲染的数据看作是表单内容,仿照demo,直接把整个markdown数据扔进MathJax.tex2chtmlPromise()函数里。这个方法在内容中只有公式的时候表现优异,还让我激动了一把。结果一加入其他dom元素就歇菜,整个dom被重组,样式也全部丢失。

3. 文档还是硬道理

google无果,能想到的方法也都试了,最终还是回归文档,看看有什么被我忽略的方法。事实证明,只有文档才能解决一切问题,我看到了一个Handling Asynchronous(异步处理)的方法MathJax.typesetPromise()

在数据渲染完成的地方,加上这个方法之后,世界就变得美好了...

  • 在内容展示页,只需要在数据返回后加上这个方法,为保证脚本在页面渲染完成后再执行,可以稍作延迟:
ajax.get(xxx).then(res => {
    // 页面渲染
    settimeout(res => {
        MathJax.typesetPromise()
    }, 500)
})

在markdown编辑区,在预览渲染后,执行渲染脚本,这里就用到了开始提到的simplemde的自定义渲染方法previewRender,同样的,为保证脚本在页面渲染完成后再执行,可以稍作延迟。其他编辑器,只要找到类似的方法也都可以这么处理:

this.simplemde = new SimpleMDE({
  {
    //...其他配置项
    previewRender: (plainText) => {
        settimeout(res => {
            MathJax.typesetPromise()
        }, 500)
        return this.simplemde.markdown(plainText)
    }
  },
  element: document.getElementById('simplemde')
})

想来,这个方法应该就是在废弃2.x版本window.MathJax.Hub.Queue之后的替代方法吧。随后,果然如此,发现了这一页,官网也有非常详细的说明。(为什么不早发现呢。T_T)

之后还处理了行内公式的识别,MathJax也出了自定义的识别方法,更多的config也都能在官网上找到。

window.MathJax = {
    startup: {
      pageReady: () => {
        // alert(111) // 检验脚本首次是否加载成功
        return window.MathJax.startup.defaultPageReady()
      }
    },
    tags: 'all', // 为方程式编号
    tagSide: 'left', // 方程式编号的位置
    tex: {
      processEscapes: true,
      processEnvironments: true, // process \begin{xxx}...\end{xxx} outside math mode
      processRefs: true, // process \ref{...} outside of math mode
      inlineMath: [
        ['$', '$'],
        ['\\(', '\\)']
      ],
      displayMath: [ // start/end delimiter pairs for display math
        ['?', '?'],
        ['\\[', '\\]']
      ]
    }

4. 对MathJax探索的总结与梳理

这整个探索流程花费了两个半天的时间。个人对于MathJax的整体机制也随着探索的过程逐渐有了更深的了解。我觉得对于后来者,更重要的应该是解决问题的思路,而不是照搬方法。所以想在这里根据我的理解,简单解释一下mathjax的机制。

上文也有提到,对于MathJax来说,有两种渲染公式的方式。我不厌其烦地再唠叨一遍:

第一种,就是我刚刚说的全局配置。首先引入CND资源,并设置好全局配置。等页面渲染好后,脚本开始执行,在网页中寻找符合条件的公式符号(比如被包裹在$中的内容),并把它编译。

你仔细看就会发现,官网的示例都给CDN加上了async,同步加载,也就是说,脚本会等页面内容都加载完成后,再执行,从而保证公式正常渲染。

但是,在像vue这些数据驱动的MVC框架下,外部资源要想对dom进行渲染,那真是太天真了,async肯定无效,这时候就需要“重新执行脚本”。在3.x版本中,也就是MathJax.tex2chtmlPromise()方法。

第二种,是比较好理解的,和katex给出的示例类似。是将指定的公式内容,全部转成公式格式。通常可以用于用户在表单中输入,用事件触发的方式,比如点击按钮,再去编译输入框中的内容,返回html。

相比mathjax,Katex我是只是初步进行了探索,很有可能我没有注意到,说不定它的第一种实现也是有。毕竟它官网列举了辣么多的优点

结语

katex,mathjax,还有simplemed,全都没有中文文档。可能开发过程中如果遇到坑了,很多人第一反应都不是翻文档(我自己也是),而是去搜索有没有人遇到类似的问题,甚至直接去github上去提issue。

而这次经历给我的经验是,适可而止的google,答案都在文档里。