mathjax库用来渲染dom结构中的latex公式。和katex相比,它支持更多的语法(排版语法等),并且有多种输出格式,包括html、svg等。笔者因为katex难以实现根据容器大小自动缩放公式的功能,mathjax支持输出svg比较方便,所以在项目中把库换成了mathjax。然而mathjax文档略混乱,网上资料又少,自己痛苦摸索了几天,在掘金记录一下用法以便后来人查阅……

本文描述了即时公式渲染和批量渲染dom两种渲染方式。

使用到的框架是react,但涉及到框架的内容并不多,用vue的同学也可以参考~

二、引入mathjax

这里使用的是目前最新的mathjax版本(3.2),网上已有的资料大部分是2.7的,大家注意区分,其中的api变化不小。

2.1 引入脚本

我们可以使用cdn引入script脚本,也可以使用npm包。使用npm包的话首先需要运行npm install mathjax@3(现在已经更新到4大版本了,但是我们还是用3),但是mathjax的npm包本质上还是加载js文件,跟引入script的配置方法几乎没有差异。在后续 startLoadMathjax 方法中对npm引入方式进行了注释。

const MATHJAX_SCRIPT_URL = 'https://cdn.bootcdn.net/ajax/libs/mathjax/3.2.2/es5/tex-svg-full.js';

需要使用promise等方法防止重复引入,重复引入会导致window.MathJax的一些方法被删除! 下面是一个例子,其中load方法是自定义加载script脚本的方法,大概就是创建一个script标签再append到body上,这里不赘述。

let initMathjaxPromise; // Promise对象,表示脚本是否加载完毕
async initMathjax() {
if (!this.initMathjaxPromise) {
  this.startLoadMathjax();
return this.initMathjaxPromise;
async startLoadMathjax() {
this.initMathjaxPromise = (async () => {
  window.MathJax = MATHJAX_CONFIG; // 这个是配置信息,后文会讲到。使用默认配置可以删掉这句。
  await load(MATHJAX_SCRIPT_URL); // 这里写自己的加载script方法
  //npm包引入写: await require('mathjax/es5/tex-svg-full.js')
  //引入mathjax或者使用import等方法都会报错,mathjax对webpack本身非常不友好
})();

mathjax的配置信息载入方式比较怪异,需要在载入脚本之前先写好配置信息。除了上述代码中用到的方法,还有其他的方式,例如单独创建一个js文件存放配置信息等,想要了解的话可以访问文末附上的参考资料查看。

2.2 配置信息

2.2.1 完整的配置信息

这里先附上笔者使用的配置信息,后边会讲点比较重要的配置。完整的配置信息见官方文档:www.osgeo.cn/mathjax/opt…

export const MATHJAX_CONFIG = {
  startup: {
    pageReady: () => Promise.resolve(), // 重写pageReady,禁止加载脚本后自动渲染
  options: {
    // 隐藏右键菜单
    enableMenu: false,
    // 忽略扫描的标签
    skipHtmlTags: ['script', 'noscript', 'style', 'textarea'],
    // 需要排除的class标签
    ignoreHtmlClass: 'class-ignore',
    // 需要处理的class标签
    processHtmlClass: 'class-process',
  // 输入配置
  tex: {
    // 自定义匹配行内数学公式的开头和结尾
    inlineMath: [ 
      ['\(', '\)']
    // 自定义匹配行间数学公式的开头和结尾
    displayMath: [  
      ['$$', '$$'],
      ['\[', '\]']
    // 解析出现错误的回调函数
    formatError: () => { 
        throw new Error('mathjax error')
  // 输出配置
  svg: {
    // 缩放比例
    scale: 1,
    // factor最小缩放比例
    minScale: 0.5,
    // 靠左水平对齐
    displayAlign: 'left',

2.2.2 startup配置

startup包含ready和pageReady两个方法,默认的方法分别是MathJax.startup.defaultReady()MathJax.startup.defaultPageReady()我们可以对他们进行重写。只需要在这里写需要重写的方法就行了。

ready表示mathjax内容初始化过程,为了能正常运作咱们还是得调用默认方法。但重写的话可以在加载前后添加一些小的修饰。下面的例子来自官方文档:

window.MathJax = {
  startup: {
    ready: () => {
      console.log('MathJax is loaded, but not yet initialized');
      MathJax.startup.defaultReady();
      console.log('MathJax is initialized, and the initial typeset is queued');

pageReady是这里的重点,它默认的行为会在mathjax就绪后立刻对整个文档进行公式字符串的替换(这个过程称为排版),所以我们最好进行重写。注意需要返回一个promise对象。

  startup: {
    pageReady: () => Promise.resolve(), // 重写pageReady,禁止加载脚本后自动渲染

2.2.3 options配置

这里着重讲解skipHtmlTags、ignoreHtmlClass、processHtmlClass三者。

mathjax的批量替换dom工作中,对于标签在skipHtmlTags中的元素,会直接跳过整个标签及其内部元素;如果某个元素的某级父元素在ignoreHtmlClass中,但自身又在processHtmlClass中,则还是会处理内部的字符串。

如果我只想渲染class"equation-container"中的元素,那么可以给整个body设置一个class,放ignoreHtmlClass中,再把"equation-container"放到processHtmlClass中。

注意,把body放到skipHtmlTags会导致整个文档无法被批量渲染;只设置processHtmlClass不设置ignoreHtmlClass会让整个文档被渲染。

options: {
    // 隐藏右键菜单
    enableMenu: false,
    // 忽略扫描的标签
    skipHtmlTags: ['script', 'noscript', 'style', 'textarea'],
    // 需要排除的class标签,多个用 | 隔开,'class1|class2|class3'
    ignoreHtmlClass: 'class-ignore',
    // 需要处理的class标签,多个用 | 隔开,'class1|class2|class3'
    processHtmlClass: 'class-process',

3.1 tex2svgPromise()

tex2svgPromise传入一个字符串,返回的是一个value是svg对象的promise对象。我们可以利用这个做一些加载态的过渡等效果。适用于随着文本变化动态更新公式渲染的情况。 如果不要promise可以直接调tex2svg()。

使用这种方法不会产生跳变,并且在任意时候都不会露出公式原字符串。这种渲染方式本身并不依赖dom,我们采用传入公式字符串后更新state,从而更新需要挂载的元素dom的方法。首次渲染和监听到props.value变化之后调用updateSvg更新我们自己指定的dom。

下面列举了react类组件中的用法,vue也是差不多的,更新data中存放svg的变量,让新的svg挂到dom上即可。也可以使用防抖等方法提高性能。

// props:value 公式字符串
class EquationDisplay extends Component{
  public override state={
     svg:'';
     isLoaded:false;
  public override render(){
      if(!this.state.isLoaded){
          return null
     return <div dangerouslySetInnerHTML={{ __html: this.state.svg }} />;
  public override componentDidMount() {
     this.init();
  public override componentDidUpdate() {
    this.updateSvg();
  public async init() {
    if (!window.MathJax) {
      await initMathjax(); // 我们之前写的载入script的方法
    this.setState({ isLoaded: true });
    this.updateSvg();
 // 异步更新dom内容
  public updateSvg = async () => {
    const { value } = this.props;
    try {
      const output = await window.MathJax.tex2svgPromise(value);
      const svgs = output.getElementsByTagName('svg');
      if (svgs.length > 0) {
        const svg = formula[0];
        this.setState({ svg: svg.outerHTML }); //注意这里需要渲染的是svg.outerHTML
      // 清除MathJax缓存
      window.MathJax.startup.document.clear();
      window.MathJax.startup.document.updateDocument();
    } catch (e) {
      console.log(e);

如果需要公式svg能够自动缩放,在父容器的样式中写上下面的样式即可:

.containter {
   svg {
       max-width: 100%;
       max-height: 100%;

3.2 typesetPromise()

上面一种方法对于单个公式渲染是比较友好的,但是在有多个独立公式dom存在的情况下,每个dom渲染都是异步的,可能会出现卡顿。对于大量需要处理的公式dom,可以使用typesetPromise()

typesetPromise()方法对我们在options中设置的dom进行批量处理,匹配元素innerHTML中的公式字符串(tex配置项中我们设置了公式字符串的开头结尾)。如果我们使用默认的pageReady(),在这个过程中也会进行一次typeset()。这个方法只处理当前文档中存在的元素,每次dom更新之后,都要重新调一次typesetPromise()才可以更新新增的公式,而每次调用mathjax都要遍历整个文档树,因此频繁变化的公式不适宜用这个方法。

由于渲染需要时间,使用这个方法刚渲染的时候会露出原有的文本内容,这个大家可以设置color:white,或者利用promise控制蒙层显示等。

  // 很多很多公式,每个都要渲染
  private equations=[
    'v = \frac { c } { n } ',
    'v = \frac { c } { n } ',
    'v = \frac { c } { n } ',
    'v = \frac { c } { n } '
  public overrde render(){
        return (
          <div className='shouldProcess'>
                //要把公式文本包裹在标签,注意前后缀
               this.equations.map((value)=>(<div>{`$$ ${value} $$`}</div>))
          </div>
  public override componentDidMount(): void {
    this.show();
  public async show() {
    await initMathjax();
    await window.MathJax.typesetPromise();

四、参考文章

  • mathjax3.2官方文档
  • 前端整合MathjaxJS的配置笔记
  • Tantanz
    粉丝