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;
async initMathjax() {
if (!this.initMathjaxPromise) {
this.startLoadMathjax();
return this.initMathjaxPromise;
async startLoadMathjax() {
this.initMathjaxPromise = (async () => {
window.MathJax = MATHJAX_CONFIG;
await load(MATHJAX_SCRIPT_URL);
})();
mathjax的配置信息载入方式比较怪异,需要在载入脚本之前先写好配置信息。除了上述代码中用到的方法,还有其他的方式,例如单独创建一个js文件存放配置信息等,想要了解的话可以访问文末附上的参考资料查看。
2.2 配置信息
2.2.1 完整的配置信息
这里先附上笔者使用的配置信息,后边会讲点比较重要的配置。完整的配置信息见官方文档:www.osgeo.cn/mathjax/opt…
export const MATHJAX_CONFIG = {
startup: {
pageReady: () => Promise.resolve(),
options: {
enableMenu: false,
skipHtmlTags: ['script', 'noscript', 'style', 'textarea'],
ignoreHtmlClass: 'class-ignore',
processHtmlClass: 'class-process',
tex: {
inlineMath: [
['\(', '\)']
displayMath: [
['$$', '$$'],
['\[', '\]']
formatError: () => {
throw new Error('mathjax error')
svg: {
scale: 1,
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(),
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'],
ignoreHtmlClass: 'class-ignore',
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上即可。也可以使用防抖等方法提高性能。
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();
this.setState({ isLoaded: true });
this.updateSvg();
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 });
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
粉丝