
简介一款基于jQuery与Mathquill的所见即所得数学公式编辑器插件面向需要在网页中展现复杂数学表达式的开发者适用于在线教育、科研论文排版、题库系统等场景。压缩包共26个文件大小仅2.79MB包含4个CSS样式文件、3个核心JS脚本含jQuery与mathquill.min.js、多套eot/ttf/woff/svg字体图标资源以及HTML示例页面和readme说明文档结构清晰便于直接集成与二次开发。已有331人学习下载。通过阅读示例与源码开发者可快速掌握编辑器初始化、公式事件监听及自定义按钮的方法并能将数学公式以LaTeX格式保存或恢复从而高效搭建交互式公式输入功能提升在线教学与学术交流的用户体验。1. 需求分析与方案选型1.1 为什么还需要jQuery——不是情怀是现实先聊一个绕不开的问题都什么年代了为什么还要用jQuery写插件我在做这个数学公式编辑器之前也纠结过这个问题。后来发现实际项目里很多内容管理后台、题库管理系统、教务系统前端技术栈仍然停留在jQuery时代而且短期内不可能整体重写。这些系统往往运行在校园网或政企内网浏览器版本落后、网络环境受限引入React或Vue意味着要搭构建链、改路由、动模板改造成本远远超出“加一个公式编辑功能”本身。jQuery插件在这种环境下有一个天然优势直接把一个JS文件和一个CSS文件引进去初始化调用一行代码不依赖npm、不依赖打包工具、不挑服务器环境。对接手老项目的开发者来说这是最稳妥的落地方式。另外jQuery对低版本浏览器的兼容性是真的好MathJax渲染公式用的DOM操作本身不复杂jQuery的选择器和事件委托机制能让代码整体更简洁维护门槛也低。1.2 编辑器形态对比——为什么非要做成“所见即所得”在动手写代码前先搞清楚一个核心问题用户到底需要什么样的输入体验我当时调研了三种主流方案。第一种是纯文本输入方案用户在textarea里敲LaTeX源码点击预览按钮才能看到渲染结果。这种方案开发成本最低但实际使用中教育类用户普遍反馈“太抽象了”——很多老师根本不知道\frac、\sqrt是什么意思打完一行代码满屏反斜杠和花括号视觉冲击力极强学习成本直接把大部分用户劝退了。第二种是弹窗选公式方案公式以模板图片形式放在面板里供用户点选点击后插入图片链接到编辑器。这种方式对不常输入的轻度用户友好但遇到要修改公式参数的情况就完全失灵——你没法把图片上的数字改掉只能删掉重新选效率很低。第三种就是今天要说的所见即所得方案。用户看到的是渲染后的公式效果点工具栏上的按钮插入符号和结构公式内部还可以再编辑。这个方案的核心思路是编辑区直接用contenteditable承载用户输入的LaTeX经过解析后渲染成MathML或SVG图形视觉上就是最终的打印效果。用户不需要理解任何语法就像在Word里用公式编辑器一样输入一个分式、一个根号看到的就是分式和根号。三种方案对比如下方案用户学习成本修改公式难度开发成本适用场景纯文本LaTeX输入高较低低程序员、论文写作者模板图片点选低高低轻度用户、固定模板所见即所得编辑器低低较高教育、题库、文档系统我最终选择做所见即所得原因很直接这个编辑器要交给一线教师使用他们的Word公式编辑器用得比谁都熟练你让他去记语法他反手就把需求打回给你。所见即所得是唯一能让非技术用户顺畅使用的方案。2. 整体架构与核心设计2.1 插件功能拆解——别看界面简单里面东西不少这个编辑器从使用角度拆核心功能有四个编辑区、工具栏、渲染器、内容输出。编辑区负责接收用户输入内部是一个contenteditable的div用户在这里面输入文本和公式工具栏排布了各类数学符号按钮包括数字运算符、希腊字母、分式根号、求和积分、矩阵等常用分组渲染器承担“所见即所得”的关键任务其实它不直接在编辑区绘制公式而是把编辑区内嵌的LaTeX代码交给MathJax去渲染再把渲染结果回填到编辑区中显示内容输出解决的是“编辑完了怎么用”的问题插件需要把最终结果转成可提交到后台的LaTeX源码同时也要提供转HTML的功能方便生成试卷或展示页面。插件内部还内置了一个简单的输入解析器用来识别用户输入内容中哪些是公式、哪些是普通文本。常见做法是用特殊分隔符包裹公式比如\[ \]或者$ $解析器扫描编辑区内容遇到分隔符就把中间的字符串交给MathJax渲染没遇到的则按普通文本显示。这种方案的兼容性好——即便用户手动在输入框里粘贴了一段带\[ \]标记的LaTeX也能被正确识别并渲染。2.2 技术选型MathJax还是KaTeX选渲染器的时候我犹豫了很久。当时主流方案有两个MathJax和KaTeX。MathJax是老牌选手支持全部LaTeX命令渲染质量极高字体平滑度和打印效果非常接近专业排版软件的输出语法兼容性几乎没有短板。缺点是体积大、渲染速度相对慢尤其是第一次加载需要下载字体文件在弱网环境下体验会打折扣。KaTeX是后来者主打高速渲染体积小加载快但对复杂LaTeX命令的支持不如MathJax完整。像一些进阶的矩阵环境、多行公式对齐、某些特殊符号KaTeX可能会直接报错或不渲染。我的选择是MathJax 2.7版本。为什么不用新版MathJax 3因为MathJax 3虽然性能更好但配置方式变化较大对jQuery插件的侵入式写法不够友好而且2.7的接口文档丰富、踩坑案例多出了问题能在网上快速找到答案。在插件场景里稳定性和可控性比版本新更重要。实际上如果你要集成到现有项目里渲染器的选择可以做成替换式的设计——插件内部封装一个render()方法想换渲染引擎时只改这个方法的实现其它代码不动。这个抽象的收益在后来维护时体现得很明显。3. 实操过程与核心环节实现3.1 插件骨架——jQuery插件的标准封装开发jQuery插件时最忌讳把一堆全局函数散落在window对象上。我采用的标准写法是基于$.fn扩展配合默认参数合并这种模式可以避免命名冲突也能让多个编辑器实例互不干扰。核心结构代码如下(function($) { use strict; var FormulaEditor function(element, options) { this.$element $(element); this.options $.extend({}, $.fn.formulaEditor.defaults, options); this.init(); }; FormulaEditor.prototype { init: function() { this.buildEditor(); this.bindEvents(); this.initMathJax(); }, buildEditor: function() { // 构建编辑器DOM结构 }, bindEvents: function() { // 绑定工具栏点击事件、输入事件 }, initMathJax: function() { // 初始化MathJax并配置tex2jax }, insertCommand: function(latexCode) { // 向编辑区插入LaTeX命令 }, render: function() { // 触发渲染并更新编辑区 }, getValue: function() { // 获取包含LaTeX源码的内容 }, setValue: function(content) { // 设置初始内容 } }; $.fn.formulaEditor function(options) { return this.each(function() { var $this $(this); if (!$this.data(formulaEditor)) { var editor new FormulaEditor(this, options); $this.data(formulaEditor, editor); } }); }; $.fn.formulaEditor.defaults { toolbar: basic, // 工具栏配置basic/full theme: light, // 主题 output: latex, // 输出格式latex/html delimiters: [\\[, \\]] }; })(jQuery);这里有几个关键细节值得注意。第一通过data缓存了编辑器实例重复初始化不会造成事件重复绑定第二每个实例持有自己的options对象副本多个编辑器互不影响第三所有方法都挂到原型上而不是闭包里方便后期扩展和调试。这套骨架通用性很强不只是数学公式编辑器其它jQuery插件富文本、图表、表格控件都可以沿用。调用方式很简洁$(#math-editor).formulaEditor({ toolbar: full, output: latex });初始化时插件会读取目标textarea或div的内容作为初始数据在原有位置插入编辑区DOM原来的元素则作为隐藏载体保存最终值这个设计对表单提交特别友好——用户在编辑器里输入后提交时还是从原来的textarea取值后端代码一行都不用改。3.2 编辑区实现——contenteditable的关键细节编辑区是整个编辑器的核心载体我使用的是contenteditable div。这里有一个非常容易踩坑的点contenteditable的默认行为在不同浏览器里有明显差异尤其Firefox和Chrome对回车键的处理方式不一样直接按回车可能在Firefox中插入的是换行而非回车。为了解决这个问题我在keydown事件中做了统一拦截当用户按下回车时如果是文字段落内就插入标签如果位于列表或公式块旁则按实际情况处理。代码逻辑大致如下this.$editor.on(keydown, function(e) { if (e.keyCode 13) { e.preventDefault(); document.execCommand(insertHTML, false, brbr); } });至于为什么用execCommand而不直接用document.createElement追加因为execCommand能把插入操作并入浏览器的原生撤销栈中用户按CtrlZ时能正常回退这是内容编辑器体验的一个隐形需求。插入公式的交互是所见即所得的灵魂。工具栏上每个按钮都绑定了一个命令码比如点击“分式”按钮就在当前光标位置插入\frac{}{}并自动把光标定位到第一个花括号里用户在页面上看到的是\[ \frac{其他内容}{} \]MathJax渲染后就是一个实实在在的分式结构。具体实现上有两种处理方式如果是全屏宽度的独立公式块使用\[ \]分隔符如果是段落中镶嵌的公式用$ $包裹。插件的delimiters配置可以按需调整。3.3 渲染链路——MathJax集成的正确姿势渲染环节是所见即所得的关键也是最容易让人头疼的部分。MathJax的渲染是异步的而且它默认只希望在页面加载时一次性处理所有公式。要在用户每次点击工具栏按钮后实时更新编辑区需要主动触发MathJax的重新渲染。我当时采用的方案如下FormulaEditor.prototype.render function() { var self this; // 触摸MathJax的队列让它在渲染结束后执行回调 MathJax.Hub.Queue([Typeset, MathJax.Hub, self.$editor[0]], function() { // 渲染完成后更新隐藏textarea的值 self.$textarea.val(self.$editor.html()); }); };MathJax.Hub.Queue是控制渲染队列的关键API它能保证渲染动作按顺序执行并在渲染完成后触发回调。这里一定要在初始化MathJax时关闭自动渲染并把tex2jax的跳过配置设好否则编辑区每一次DOM变化都可能触发重复渲染性能会很差。我整理的初始化配置长这样MathJax.Hub.Config({ tex2jax: { inlineMath: [[$, $]], displayMath: [[\\[, \\]]], processEscapes: true, skipTags: [script, noscript, style, textarea, pre, code] }, showProcessingMessages: false, messageStyle: none, skipStartupTypeset: true });skipStartupTypeset: true这个配置非常重要它让MathJax在页面加载时不对整个文档进行扫描渲染完全由插件控制渲染时机避免和其它富文本内容冲突。还有个细节是渲染后的样式被MathJax自动添加了样式属性直接取editor.innerHTML提交到后台会包含一堆嵌套的span和MathJax私有样式。我的处理是取内容时先克隆一份DOM把math class的元素还原成LaTeX源码再拿出来这个逻辑在getValue()里实现保证最终提交的数据干净整洁。3.4 工具栏实现——符号面板的分层设计工具栏看似是纯UI工作实际上承载了编辑器的易用性。我把工具栏分成两层一级工具栏陈列常用符号包括加减乘除、等于不等、分数根号等高频操作点按直接插入二级面板通过下拉方式展开收纳分式类、根号类、求和类、积分类、希腊字母类等符号分组按需切换。按钮数据结构我用JS对象配置var toolbarConfig { fraction: { label: 分式, latex: \\frac{}{}, category: structure }, sqrt: { label: 根号, latex: \\sqrt{}, category: structure }, sum: { label: 求和, latex: \\sum_{i1}^{n}, category: large }, // ... 其它符号 };生成按钮时遍历配置对象事件绑定使用jQuery的事件委托只在工具栏容器上绑定一次click事件然后通过data属性区分按钮对应的latex命令。这样就算后续动态添加新按钮也不用重新绑定事件。实际使用中工具栏里加一些常用特殊结构绝对值、极限、矩阵能显著提升满意率因为用户不用为了一个极限符号去翻好几级菜单。4. 常见问题与排查技巧实录4.1 渲染失效与延迟的排查我在集成测试阶段遇到最典型的问题是点击分式按钮后编辑区里显示了\frac{}{}源码但MathJax迟迟不渲染成公式。排查后发现原因有两个。一是初始化时忘了把skipStartupTypeset设为trueMathJax在页面加载阶段就和编辑器抢渲染权结果编辑区里的内容没有被识别。二是MathJax渲染需要一个去抖机制用户连续点击按钮插入多个命令时每次都直接调Queue会导致渲染队列积压界面卡顿严重。针对第二个问题我的解决方案是在insertCommand时设置一个200毫秒的定时器连续插入命令只会触发最后一次渲染var renderTimer null; this.$toolbar.on(click, .symbol-btn, function() { if (renderTimer) clearTimeout(renderTimer); renderTimer setTimeout(function() { self.render(); }, 200); });这个优化之后连续插入多个符号时页面始终保持流畅渲染结果也正确。MathJax渲染大批量公式时本来就偏慢在插件层面做一次合并处理是非常有效的优化手段。4.2 光标位置丢失的修复contenteditable编辑区有一个通病当你通过代码在光标处插入HTML后浏览器会自动把光标移到容器末尾用户的输入焦点就丢了。尤其是用户想在公式前面加文字点击分式按钮后焦点跳到末尾打字打到公式后面体验非常糟糕。解决的方法是用selection和range API保存位置在插入操作后恢复光标。这里有一个比较成熟的实践function insertAtCursor(editor, html) { var sel window.getSelection(); if (sel.rangeCount 0) { var range sel.getRangeAt(0); range.deleteContents(); var el document.createElement(div); el.innerHTML html; var frag document.createDocumentFragment(); var node el.firstChild; var lastNode frag.appendChild(node); range.insertNode(frag); // 把光标移动到插入内容的末尾 range.setStartAfter(lastNode); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); } else { editor.focus(); } }这里最关键是杀器是setStartAfter它能让光标准确落在插入内容之后而不是容器末尾。实测下来这个方案在Chrome、Firefox、Edge下表现一致Safari偶尔有偏差但整体可接受。4.3 公式与普通文本混排的边界处理另一个高频问题出在公式和汉字混排的场景。默认配置下inlineMath用美元符号包裹系统里输入人民币金额“$100”时会被误判为公式开始标记导致内容渲染错乱。我一开始没意识到这个坑直到测试人员用“价格$500起”做测试用例时发现了问题。排查之后我调整了分隔符策略正文中的行内公式改用\( \)包裹块级公式维持\[ \]不变并在tex2jax配置中关闭美元符号作为行内公式标记的识别inlineMath: [[\\(, \\)]], processEscapes: true这样对普通用户的输入习惯影响最小同时又防止了美元符号冲突。如果项目中确实需要美元符号作为公式标记则需要在后端做一层转移处理但没有特殊需求不推荐。4.4 与表单提交的集成要点编辑器是给后台管理系统用的最终数据要提交到服务器。我的做法是初始化时把目标textarea隐藏编辑器的每一次渲染都同步HTML和LaTeX源码到textarea的value中。这样在表单提交时浏览器自动把textarea的value发送给后台不需要额外写取值逻辑。有一个坑要提醒有些后端框架在接收包含HTML标签的内容时会自动转义导致保存的公式源码变成一堆span之类的实体字符。处理办法是后台对公式字段单独设置标记或者在保存前对内容进行反转义也可以用JSON字段存储。我在对接一个Java后端时遇到过这个问题前端无论怎么改后台存进去的数据都是转义后的乱码最后是后端配合修改了字段注解才解决。4.5 常见问题速查表问题现象可能原因解决方案公式长时间显示源码MathJax加载延迟或初始化失败检查CDN地址确认queue方法调用顺序插入按钮无反应事件绑定失效或编辑器实例重复创建检查data缓存确认工具栏容器事件委托光标跳到文章末尾未保存并恢复range使用selection API保存光标位置页面加载卡顿MathJax对整个文档扫描渲染配置skipStartupTypeset:true提交后台乱码后端框架转义HTML实体后端调整字段存储格式或前端JSON编码复制的LaTeX不渲染缺少分隔符标记输入时自动补充$或\[ \]标记5. 实用小技巧与扩展方向5.1 让编辑体验加分的细节编辑器做出来能用和好用是两回事。在打磨阶段我加了好几个提升体验的小功能每一个成本都不高但用户反馈非常好。第一个是快捷键支持。CtrlB加粗、CtrlI斜体这种是富文本标配公式编辑器里还可以加CtrlG插入分式。实现方式是在keydown事件里做判断然后调用insertCommand对应的方法。这里的难点是需要防止浏览器默认行为比如在Mac上CtrlB和CommandB是不同的快捷键组合要做兼容处理。第二个是拖拽调整公式大小。MathJax渲染出来的公式本质是HTML内联元素通过CSS的font-size属性可以整体缩放。我给编辑区加了一个右下角的拖拽手柄用户拖拽时动态调整编辑区的font-size公式和文字同步缩放视觉效果直观。这个功能的实现很轻量就是对mousedown、mousemove、mouseup三个事件的处理。第三个是公式复制友好的右键菜单。很多用户习惯从别的地方复制公式内容粘贴进来但直接粘贴往往带了一堆无关样式。我在编辑器上做了一个自定义的paste事件处理粘贴时检测内容类型如果是纯文本则自动保留文本并重新格式化如果是HTML则抽取其中的公式部分。这个功能解决了教育类用户的真实痛点——他们经常从Word文档里复制公式内容在浏览器里直接粘贴会带大量Word样式垃圾。5.2 插件还可以往哪些方向扩展这个公式编辑器目前已经在生产环境稳定运行了很长时间累计服务了几千名教师用户。回头来看如果项目再有迭代空间有几个方向值得优先考虑。第一个方向是预设公式模板库。把高中和大学数学里的常见公式做成模板分类存放用户通过搜索或分类浏览快速选择一次点击插入整段公式。从使用频率看中位数用户每天插入的公式数量并不高但90%都集中在若干个固定结构上模板库能显著降低操作次数。第二个方向是导出为图片。MathJax渲染结果可以借助html2canvas或MathJax自身的SVG输出能力转成图片这个功能在题库系统和试卷打印场景下非常实用。后端拿到LaTeX源码后渲染排版是一回事用户想直接把公式截图贴到别的文档里是另一回事。第三个方向是协同编辑。如果后续系统要从单机编辑升级为多人同时编辑一套试卷公式编辑器的数据结构就要改造需要给每个公式块增加唯一标识协同层通过操作日志同步不同成员的光标和公式修改。我还没实现过完整的协同功能但根据现有架构来看基于全局唯一的DOM节点ID做增量同步是可行路径。6. 写在最后的几点体会做这个公式编辑器让我最大的收获不是摸透了MathJax的队列机制也不是学会了selection API的花式用法而是想清楚了一件事前端插件的价值从来不在于技术栈本身新不新而在于它能不能在真实场景里帮用户解决问题。在一个老旧的jQuery项目里用jQuery完成一个所见即所得的数学公式编辑器比强行引入React再写一遍适配壳要实际得多。很多开发者面对老项目时总想着“推倒重来”但现实是绝大多数业务系统都不会因为前端框架旧就停止运行渐进式增强、在既有体系内提供优质功能往往才是性价比最高的方案。最后再分享一个排查技巧调试contenteditable相关的问题时建议在Chrome的DevTools里打开“Event Listener Breakpoints”把clipboard、keyboard、mouse三类事件全部打上断点逐步观察事件触发顺序和DOM变化。很多怪异的行为——比如插入公式后焦点丢失、粘贴格式混乱、按钮点击无反应追查时看一遍完整的事件链路原因立刻清晰起来。这个方法帮我节省了大量的排查时间值得一试。本文还有配套的精品资源点击获取