简介:这是一款基于jQuery的所见即所得数学公式编辑器插件,适用于教育平台、科研站点和在线测评系统,解决网页中复杂数学公式输入与排版难的问题。以MathQuill为核心,用户通过点击图标或键盘输入即可直观构建LaTeX公式,支持乘方、开方、积分、矩阵及希腊字母等常用结构。压缩包共26个文件、约2.79MB,主要包含CSS样式、JavaScript脚本、TTF/EOT/WOFF字体、HTML示例及预览图片等,JS涵盖编辑器主逻辑和jQuery依赖,字体用于公式符号渲染,示例页面可快速预览效果。已有333人学习下载。资源内提供完整源码、可直接运行的demo页面和说明文档,开发者可据此掌握初始化、事件监听及自定义扩展方法,便于直接集成或二次开发,也适合前端工程师研究所见即所得编辑器的实现思路。
1. 所见即所得的数学公式编辑器:为什么表单里的公式输入不该再做“苦力学”
做在线表单、问卷、作业批改这类系统时,公式输入一直是个让人头大的需求。用户要的不是在文本框里敲一串\frac{a}{b},而是想看到分数、根号、上下标像书一样显示在输入位置,光标还能在公式结构里来回移动。所见即所得的 jQuery 数学公式编辑器插件,解决的就是这个矛盾:它把公式编辑变成“打一个字、渲一个像”的交互,底层同时维护一份 LaTeX 源码,供后端存储和排版。
这篇文章面向正在为老项目选型、或者不想引入重前端框架的团队。我会从编辑器的工作机制讲起,给出最小可用的初始化代码、常用参数、输出采集方式,再集中讲几个我实际踩过的坑,最后补一些自定义符号面板和模板插入的技巧。整体按一套可以用起来的方案来写,你不需要改造现有页面结构,也能把这个编辑器接进任意表单。
2. 编辑核心与渲染链路:先想清楚光标、DOM 与 LaTeX 的三方契约
2.1 为什么“所见即所得”比“文本框+预览”更合适
公式输入的常见实现有三种:双栏预览、源码编辑、所见即所得。双栏预览是左边输源码右边看渲染结果,实现简单但用户要来回转移视线,在手机上一屏放不下两个区域;源码编辑只适合会 LaTeX 的用户,普通用户连\sqrt{}的括号匹配都搞不定;所见即所得则是把输入和渲染放在同一个可编辑区域里,输入时直接操作 DOM 树中的公式节点。
我倾向把第三种作为默认方案,原因是它把“用户是否懂 LaTeX”这个门槛降到了最低。符号面板点一个“分数”按钮,页面上立刻出现一条分数线,光标自动落到分母,用户继续打字就行;光标移动也遵循数学结构的语义——从分子跨到分母,是按“结构”“跳转”,而不是按字符平移。这个体验一旦用上,就很难退回原来的双栏预览了。
这套机制的底层,本质上是一个装了数学字体和排版引擎的可编辑容器。它不是用contenteditable直接裸奔,而是对键盘事件、光标位置、DOM 节点做了一层封装。你输入一个字符,先把它挂到正确的数学节点下,再触发一次渲染;你按方向键,先判断当前所处的数学语境,再决定把光标移到上一个兄弟节点还是父节点。
2.2 三种实现方案的成本对比
选型时我做了个小对比,前两种在一些内部工具里还在用,但都不适合作为对外产品的主输入方式。
| 方案 | 用户门槛 | 实现成本 | 排版质量 | 典型场景 |
|---|---|---|---|---|
| 双栏实时预览 | 中(要懂部分 LaTeX) | 低 | 最终结果一致 | 后台配置、Markdown 编辑器 |
| 源码编辑器 + 语法高亮 | 高(必须懂 LaTeX) | 低 | 最终结果一致 | 专业用户、LaTeX 文档工具 |
| 所见即所得 DOM 编辑 | 低(点按钮 + 打字) | 高 | 依赖排版引擎 | 问卷、作业、答题、表单 |
这里的关键点是“排版质量”。双栏预览和源码编辑最终都走同一个渲染器,所以只要用户能写对,输出一定标准;所见即所得方案必须自己处理行内公式、行间公式、字体尺寸、算子间距,工作量比前两者大不少。所以如果团队只是临时做个内部工具,我反而建议用双栏方案,不折腾。
但如果你做的是面向普通用户的表单系统,所见即所得是唯一能让用户“无感上手”的选择。成熟的 jQuery 公式编辑器插件已经把 DOM 编辑和 LaTeX 解析这两层封装好了,你的工作就退化成“初始化插件 + 读取结果”,这也是我推荐直接基于插件改造的原因。
2.3 核心链路:从键盘事件到 LaTeX 源码的流转
一个完整按键的流转大致是:键盘事件 → 语义命令解析 → 操作公式 DOM 树 → 重新渲染可见区域 → 同步更新内部 LaTeX 缓存。插件对外暴露的是一个编辑区容器,内部却同时维护着三个状态:当前 DOM 树、光标上下文、LaTeX 源。三者必须保持一致,任何一处不同步都会出现“显示正常但导出乱码”的怪问题。
// 结构示意:编辑区内部的三态同步关系 const editorState = { domTree: null, // 当前公式的 DOM 结构 cursor: { // 光标所在的数学环境 parentNode: null, // 父节点(如 fraction、sqrt) offset: 0 // 在节点内的偏移 }, latexCache: '' // 与 DOM 同步维护的 LaTeX 源 }; // 每次输入后的同步动作(伪代码) function onInput(latexCommand) { mutateDomTree(editorState.domTree, editorState.cursor, latexCommand); render(editorState.domTree); editorState.latexCache = generateLatex(editorState.domTree); }看到这个结构,你应该能理解一个问题:为什么插件必须监听键盘事件而不是input事件。因为公式编辑区的很多操作不产生字符输入,比如按方向键从分子移动到分母、按退格删除整个根号外层、点击分数线的某一段定位光标,这些都要在语义层面处理,单靠标准input事件拿不到足够信息。
参数层面,大部分所见即所得编辑器插件都会暴露两个方向的接口:一个是“读取”方向(latex()获取当前源码),一个是“写入”方向(latex("\\frac{1}{2}")将已有源码解析并渲染到编辑区)。写入接口很有用,是做编辑回显(比如用户提交后返回上一页修改答案)的唯一手段,后面集成时会专门用到。
3. 最小可用集成:一段能跑起来的表单接入流程
3.1 引入资源与初始化编辑器
先讲依赖。所见即所得公式编辑器不是单文件组件,它通常依赖一个样式表和一套数学字体。如果字体没加载成功,公式会显示成乱码或者占位方块,这是接入期最容易翻车的点。引入时把样式放在页面头部,脚本放在 jQuery 之后,顺序错了会导致插件找不到 jQuery 对象。
<!-- 页面头部引入样式,注意顺序:先 CSS 后 JS --> <link rel="stylesheet" href="math-editor.css"> <!-- jQuery 必须在编辑器脚本之前引入 --> <script src="jquery.min.js"></script> <script src="jquery.math-editor.js"></script>如果你的项目没有用打包工具,直接在 HTML 里按上面顺序引入即可;如果用 webpack 或 vite,常见做法是在入口文件里import 'math-editor.css',并确保 jQuery 被挂到全局window上。插件初始化很简单,把目标textarea或div转成编辑区即可。
<textarea id="formula-input" rows="2" placeholder="点击此处输入公式"></textarea>// 初始化:将 textarea 转换为所见即所得编辑区 $('#formula-input').mathEditor({ latex: '', // 初始公式,编辑回显时传入已有 LaTeX fontSize: '16px', // 编辑区内公式的显示字号 displayStyle: false, // false 表示行内样式,true 表示独立成行 spaceBehavesLikeTab: true // 按空格键时跳出当前结构(类似 Tab 行为) });逻辑说明:latex参数用于编辑回显,传入空字符串就是空白编辑区;fontSize控制公式在编辑区里的渲染大小,它不影响最终导出 LaTeX 的语义;displayStyle决定渲染时用行内样式还是独立成行的样式,如果你做的是答题系统,建议设置成false,让公式在文本行里自然穿插。spaceBehavesLikeTab是体验向参数,开启后按空格可以在分子/分母/根号内部完成后跳出当前结构,减少鼠标点击次数。
3.2 读取编辑结果:别用 textarea 的 value
这里有一个非常反直觉的点:编辑器虽然由textarea初始化而来,但你不能靠$('#formula-input').val()拿到公式源码。初始化之后,原来的textarea被隐藏或替换,真正的编辑区是一个可编辑的<div class="math-editor">,源码存在插件的内部状态里。读取结果必须调用插件方法。
// 提交表单时获取公式 LaTeX 源码 $('#answer-form').on('submit', function(e) { e.preventDefault(); // 通过插件实例获取当前编辑区的内容 var latexStr = $('#formula-input').mathEditor('latex'); // 判断用户是否真的输入了内容 if (!latexStr || latexStr.trim() === '') { alert('请先输入公式'); return; } // 后续上报给后端,例如作为问卷答案的一部分 $.ajax({ url: '/api/answer/save', method: 'POST', data: { questionId: $('#question-id').val(), answer: latexStr } }); });逻辑说明:调用方式是一个方法名字符串而非传参,这是 jQuery 插件常见的两种风格之一。mathEditor('latex')是“获取当前值”,mathEditor('latex', '\\frac{1}{2}')是“把指定 LaTeX 渲染进编辑区”。提交前要判空,因为空公式和“用户没输入”是不同的状态,前者可能是用户故意输入了一个空根号,后者是编辑区真的没有内容,推荐按你的业务语义区分处理。
参数上,还有几个值得关注的配置项:operators可以自定义显示的运算符列表,symbols控制符号面板展示哪些分类;history是内置撤销重做开关,一般保持默认开启。大部分插件还支持onChange回调,可以用于回车时动态更新页面上的公式预览。
3.3 编辑回显:把已存 LaTeX 渲染回编辑区
回显是改造老系统时最常遇到的需求:用户之前提交过答案,现在进入修改页面,编辑区里应该重现他当时写的公式。这个场景如果用val()赋值就彻底翻车了,正确做法是把 LaTeX 传给插件的写入方法。
// 进入编辑页时,从后端取已保存答案并渲染 $.get('/api/answer/detail', { id: editId }, function(res) { var savedLatex = res.data.answer; // 例如:\frac{a}{b} + \sqrt{x} // 写入编辑器;务必放在初始化完成之后 $('#formula-input').mathEditor('latex', savedLatex); });这里要提醒两件事。第一,写入用的 LaTeX 必须是插件能解析的语法范围,像\begin{cases}这类环境是否支持要看插件的解析能力,建议在测试阶段把你的历史数据全部跑一边回显,看看有没有解析不了的公式;第二,不要在初始化参数latex和主动调用写入方法两处同时赋值,否则会出现重复渲染或光标异常,我见过 A 同学在初始化时传了值,又在 ajax 回调里再赋一次,结果每个公式都闪烁了两遍才定形。
3.4 表单序列化与多编辑器共存
一个页面通常不止一个公式输入框。问卷里可能每道题都带一个公式编辑区,而页面提交用的是表单序列化。这里需要把编辑区内容同步回原始textarea,才能让serialize()正常工作。这个步骤如果不做,后端收到的是空字符串。
// 提交前把所有 math-editor 的内容同步到各自的 textarea $('#question-form').on('submit', function() { $('.math-editor-host').each(function() { var $host = $(this); var latexValue = $host.mathEditor('latex'); // 找到对应的隐藏 textarea,并写入 LaTeX 源码 $host.prev('textarea').val(latexValue); }); // 此时 serialize() 能正确带上公式内容 });逻辑说明:.math-editor-host是你给所有编辑区容器加的类名;.prev('textarea')依赖 DOM 结构中 textarea 紧挨在编辑区前面。如果你的页面结构不一样,就给初始化按钮加>var isComposing = false; $('#formula-input').on('compositionstart', function() { isComposing = true; }).on('compositionend', function() { isComposing = false; }); // 提交时如果还在组合状态,延迟读取 $('#answer-form').on('submit', function(e) { if (isComposing) { e.preventDefault(); alert('请先完成输入法组合再提交'); } });
4.3 点击符号面板按钮,编辑区光标丢失
现象:用户输入了半截公式,点了一下符号面板里的“根号”按钮,返回编辑区继续打字时,光标没有停在他原来输入的位置,而是跳到了公式开头,或者新输入的内容插到了意想不到的地方。
原因:符号面板按钮的mousedown事件默认会先把焦点从编辑区移走,编辑器在收到插入命令时又尝试恢复光标,但恢复依据的缓存位置可能已经被失焦事件清掉了。
解决:给符号面板的mousedown事件加preventDefault()。这样做的好处是焦点不会从编辑区移走,光标位置得以保留。如果你用的插件封装了符号面板、但没暴露这个配置,就在插件外手动绑定事件并阻断默认行为;如果插件用的是click事件而不是mousedown,就没这个问题,但很多插件偏偏用mousedown模拟即时响应。
// 自定义符号面板按钮:阻止 mousedown 导致的失焦 $(document).on('mousedown', '.symbol-btn', function(e) { e.preventDefault(); var command = $(this).data('latex'); // 如 \sqrt $('#formula-input').mathEditor('write', command); });4.4 导出 LaTeX 里的反斜杠被后端转义吞掉
现象:前端拿到latexStr是\frac{1}{2},通过 ajax 传给后端后,入库变成了frac{1}{2},或者\\frac{1}{2},再回显时公式无法解析。
原因:反斜杠在 JSON、Java/Golang 字符串、数据库转义逻辑里都可能被二次处理。有些语言里\f会被当成换页符处理,这就变了。公式里的\frac一旦有一个斜杠被吞,整个 LaTeX 就是非法语法。
解决:前后端约定统一传输格式,比如走 JSON 时反斜杠要正确转义为\\;后端存储原样保存,读取时不做额外反转义。如果公式要传进 HTML 的value属性或>// 前端:把 LaTeX 编码成 JSON 安全格式 var payload = JSON.stringify({ answer: latexStr }); $.ajax({ url: '/api/answer/save', method: 'POST', contentType: 'application/json', data: payload });
后端接收端只做一件事:原样存储。凡是“觉得不对、帮忙处理一下“的逻辑,在这个环节一律砍掉。
4.5 公式回显时在富文本编辑器里二次污染
现象:公式在富文本区域(如普通的contenteditable或某些老旧的富文本编辑器)里嵌套使用时,回显的公式会被富文本编辑器格式化,出现多余<span>标签,或者 LaTeX 源码被转成纯文本显示。
原因:富文本编辑器和公式编辑器的可编辑区域冲突了。公式编辑器接管了键盘事件,但富文本编辑器也监听了同样的区域,两个系统同时操作同一段 DOM 时互相覆盖。
解决:架构上隔离——富文本里只存放一个占位符,比如[FORMULA],公式内容单独存到隐藏域;展示时前端把占位符替换成公式渲染后的 HTML,提交时再转成 LaTeX。这是目前比较稳妥的做法,兼容性最好。不要试图让两个编辑器共享同一个可编辑区域,那是一场双输的博弈。
5. 进阶用法:自定义符号面板与命令模板,让编辑区更贴近业务
到了这一章,我默认你已经把基础接入跑通了。接下来要做的是把编辑器从“通用公式工具”变成“贴合业务场景的输入组件”。常见的诉求有三个:让用户少翻页就能点到常用符号、给公式套用预设模板、对输入的公式做有效性校验。这三个手段可以单独做,也可以组合。
自定义符号面板是最容易出效果的。教务场景里,用户高频使用的是\frac、\sqrt、\pi、\leq这几个;而做概率统计的问卷,\sum、\int、\infty的使用频率完全不同。默认面板是按通用数学符号排的,我们可以在外面挂一个常用栏。
// 扩展自己的常用符号栏:点击后在光标处插入对应 LaTeX var customSymbols = [ { label: '分数', latex: '\\frac{ }{ }' }, { label: '根号', latex: '\\sqrt{ }' }, { label: '求和', latex: '\\sum_{ }^{ }' }, { label: '无穷', latex: '\\infty' }, { label: '属于', latex: '\\in' } ]; customSymbols.forEach(function(item) { var btn = $('<button type="button">').text(item.label); btn.on('mousedown', function(e) { e.preventDefault(); $('#formula-input').mathEditor('write', item.latex); }); $('#custom-symbol-bar').append(btn); });我建议把mousedown的preventDefault()作为固定操作,这和 4.3 节里讲的光标丢失问题是同一个原理。'write'方法是插入语义,不是替换全文,注意和'latex'赋值区分:write是在当前光标处插入命令,latex是全文覆盖。写模板时使用带空花括号的写法,插件通常会在数学语境里自动把光标定位到第一个{ }内部。
再讲一个验证技巧。公式编辑器输出的 LaTeX,在提交给后端前先做一次“可解析性”校验比入库后再报错成本低得多。校验方式很简单:把得到的 LaTeX 传给插件的解析器,能渲染成功且渲染结果不为空,说明语法至少是可读的。
// 提交前的公式语法校验 function isValidLatex(latexStr) { if (!latexStr) return false; // 临时的隐藏验证容器,避免影响当前页面 var $validator = $('<div>').appendTo('body').hide(); try { $validator.mathEditor('latex', latexStr); var parsed = $validator.mathEditor('latex'); return parsed && parsed.trim() !== ''; } catch (e) { return false; } finally { $validator.remove(); } }逻辑说明:$validator.mathEditor('latex', latexStr)是写入,紧跟着再读一次。正常情况下读出的内容和写入的一致;如果写入的 LaTeX 存在解析不了的结构,插件会把无法解析的部分丢弃或报错,此时读出结果会变短或为空。用临时容器校验的好处是不干扰当前正在编辑的公式,缺点是额外初始化了一个隐藏编辑器,如果调用频率很高会有性能开销。
最后一个实用技巧是“公式块快速格式化”。很多插件给编辑区设置了固定的行高和字体,导致插入的复杂公式比如矩阵、分段函数会溢出到行外。遇到这种情况,不要试着靠 CSS 强行改高度,而是给编辑区容器加一个自适应样式,让容器高度根据公式块高度自动撑开:
.math-editor { min-height: 48px; height: auto !important; overflow: visible !important; }这个样式表要放在插件默认样式之后才能覆盖成功。别问我为什么不直接改插件的源码——改插件源码的代价是升级版本时所有改动要重来一遍。覆盖样式是性价比最高的方式。
回看整个接入过程,我最大的感受是:不要盲目追求功能全部放开。公式编辑器是一个天然容易“过度设计”的组件,符号面板塞下几百个符号、每个按钮都带快捷键,反而让用户找不到常用功能。我在给某问卷系统做改造时,只保留了分数、根号、上下标、求和、不等式这几个高频能力,自定义了一个极简面板,结果用户反馈比原先全量面板还正向。技术选型上少即是多,交互设计上同样如此。
各版本插件的 API 名称不完全一致,但大体思路相同:初始化、读值、写值、插入命令、销毁。等你熟悉了这一套之后,遇到内部自研的编辑器也能快速上手。希望这篇文章能帮你把公式输入这块硬骨头顺利啃下来。
本文还有配套的精品资源,点击获取