简介:面向网页开发者的所见即所得的jQuery数学公式编辑器插件,基于开源的Mathquill库打造,支持鼠标点选与键盘输入,可直观构建LaTeX格式的复杂数学公式,适用于在线教育、科研协作及需要展示数学表达式的各类网页场景。压缩包共26个文件,包含核心JavaScript脚本、CSS样式表、woff/eot/ttf等多种字体格式以及示例页面,其中JS实现编辑交互逻辑,CSS负责渲染样式,字体资源确保跨平台显示效果,开发人员可快速引用并集成。包体仅2.79MB,轻量易部署,已有333人学习借鉴。资源内附源码、完整示例页面与相关说明文档,可帮助开发者深入理解编辑器初始化流程、事件监听机制与自定义扩展方法,便于按需改造或嵌入现有项目,有效提升网页端数学公式输入的交互体验。
1. 所见即所得的 jQuery 数学公式编辑器:让表单里的公式像在文档里直接编辑
做在线题库、作业系统这类产品时,最容易被低估的一块是「公式输入」。所见即所得的 jQuery 数学公式编辑器,把填空题旁边那个只能粘图的输入框,变成可以点按钮写分式、根号、上下标的在线公式区。老师不需要知道\frac{}{}代表什么,用户点一下「分数」按钮,分子分母两个空位就出现在页面上,光标直接落在分子里;开发者只需要在编辑结束时通过一行 API 取回标准 LaTeX 字符串,存库、检索、批改全都有了解析基础。
这个方案适合手里有 jQuery 存量项目、想给表单或富文本编辑器补上公式能力的前端开发者。如果你正被「老师不会写 LaTeX」和「图片公式没法进数据库」两头夹击,这篇文章值得看完。我按选型理由、最小集成、参数调优、高频翻车点这个顺序,把整个方案拆开讲一遍,最后给一个能直接搬进项目的组件化封装。
2. 选型与原理:为什么「所见即所得」比「LaTeX 源码 + 预览」更适合表单场景
2.1 三种公式输入方案的真实差异:从使用者视角和开发者视角分别算账
给系统加公式输入之前,先看清三种方案的账。第一种是 LaTeX 源码输入框配一个实时预览区,左边写\frac{1}{2},右边显示排版结果。这种方式开发者最喜欢,因为源码可批量生成、可版本管理、可程序化校验,但把页面交给使用者时问题就大了。学科老师看到一排反斜杠和花括号,第一反应是关掉页面;语法错一个花括号,整段预览白屏,使用者完全不知道问题出在哪个字符上。它的价值更多体现在开发者自用、批量录题这类场景。
第二种是截图上传或从文档里粘贴公式图片。使用者上手成本最低,老师确实舒服了,但系统拿不到任何结构化内容。图片不能检索、不能批改、不能自适应排版,题目想改一个数字只能重新截图替换。这类方案只适合做打印物料的素材录入,完全不适合要解析公式语义的题库、作业和测验系统。
第三种就是标题里的所见即所得编辑方式:页面直接呈现排好版的公式,分数、根号、上下标都是可点进的占位空位,使用者不需要知道任何 LaTeX 语法,开发者只需要在编辑结束时调用latex()取一份标准字符串。它的成本在编辑器本身——依赖引入、兼容性处理、工具栏设计都要花时间,但这是唯一能同时满足「人能输入」和「系统能解析」两个条件的方案。
| 方案 | 使用者上手成本 | 公式可再编辑 | 后端可解析 | 典型场景 |
|---|---|---|---|---|
| LaTeX 源码 + 预览 | 高,语法门槛劝退 | 完整 | 完整 | 开发者自用、批量录入 |
| 截图 / 图片上传 | 最低 | 不可再编辑 | 不可解析 | 打印物料、临时提交 |
| 所见即所得编辑器 | 低,点按钮生成模板 | 完整 | 完整 | 题库、作业、表单输入 |
选型其实在需求阶段就已经定了:只要「使用者不是开发者」并且「公式要进数据库被程序消费」,所见即所得就是唯一合理的答案。别再为了省两天集成时间把公式输入做成图片上传,后面解析、批改、题库去重的坑会成倍补回来。
2.2 所见即所得内核的工作原理:不是排版 HTML,而是维护一棵公式结构
我最初也以为所见即所得就是把 LaTeX 字符串解析成 HTML 再放进可编辑区。真正看过开源内核的实现才发现,它的架构和普通富文本编辑器完全不同:编辑器不让你直接编辑排版后的 HTML,而是把每次输入解析成一棵数学结构树。分式是一个节点,左半部分是分子、右半部分是分母;根号是一个带内部子节点的容器;上下标是带基准和角标的节点。你在页面上看到的所有错落排版,都是这棵树渲染出来的投影。
屏幕上的渲染层由内核用带mq-前缀 class 的 HTML 生成,但这层 HTML 只是投影,从来不是数据源。真正对外暴露的数据通道只有两个:latex()把结构树序列化成 LaTeX 字符串,latex(string)把字符串反序列化回结构树。所有编辑操作——点工具栏、敲键盘、按删除——都是先改结构树,再触发重新渲染,所以「所见」和「所得」始终一致,不会出现预览和源码对不上的问题。
这个「结构层和渲染层分离」的设计,解释了后面很多坑的来源。最常见的是有人存库时图省事,用.html()把带mq-fraction这类内部 class 的 DOM 一起存进去,下次回显时换一个编辑器就整段乱掉。正确做法从来没有变过:读用latex(),写用latex(str),永远不要碰内部 HTML。把这一条记住,第 5 章一半的坑基本不会踩到。
2.3 为什么 jQuery 生态里这类方案能低成本落地
现在的前端新项目很少主动选 jQuery,但存量后台、老运营系统里 jQuery 依然是大头。给这类项目加公式输入,如果为了一小块功能引入一个重框架,要处理构建链路、路由共存、新旧渲染层冲突,成本远超公式编辑器本身。这种场景下,jQuery 生态里成熟的公式编辑器内核几乎是唯一务实的选择——它本身就是以 jQuery 插件形态发布的:一个 CSS 文件负责排版,一个 JS 文件负责逻辑,引入顺序对了就能用,不需要 npm 和打包。
对已经加载了 jQuery 的页面来说,额外成本几乎可以忽略:样式表加一行link,脚本加一行script,初始化代码十几行。这个特性在维护期特别值钱——老项目通常没有完整的构建体系,让你为一个小功能去配置 Webpack 或 Vite 工作流,运维层面就会先否决。
我也要诚实补一句选型边界:如果是从零开始的新项目,团队没有历史包袱,我会选更新的现代公式库,它们不依赖 jQuery、移动端支持更好。但对「已有 jQuery 后台、要快速补一个公式输入」的需求,基于 jQuery 的所见即所得内核就是成本最低、风险最小的路线。后面的集成方式,全部按这条路来讲。
3. 集成落地:在 jQuery 页面里跑通最小可用公式编辑器
3.1 资源引入与基础结构:CSS 顺序错了公式会散架
集成第一步,把三件事按顺序做对:样式、脚本、页面结构。样式文件必须在初始化前加载完成,否则内核初始化时拿到的是 0 高度的容器,后面公式排版会整体错位;jQuery 必须在插件脚本之前引入,这个顺序错了会直接走到第 5 章最后一条坑。
<!-- 1. 样式必须先于初始化加载 --> <link rel="stylesheet" href="/libs/mathquill/mathquill.css"> <!-- 2. jQuery 在前,插件脚本在后 --> <script src="/libs/jquery/jquery.min.js"></script> <script src="/libs/mathquill/mathquill.js"></script><div class="formula-editor"> <div class="toolbar" id="mq-toolbar"> <button type="button">// 先拿到内核命名空间。第二参数是接口版本,按当前库文档确认 var MQ = MathQuill.getInterface(2); // 初始化:把 div 变成公式输入区,返回字段对象 var field = MQ.MathField($('#math-input')[0], { autoCommands: 'sqrt sum int pi theta', // 键入这些名字后自动转成命令 handlers: { edit: function () { // 结构发生变化的回调 syncLatex(); // 把最新 LaTeX 同步到隐藏域 } } }); function syncLatex() { $('#formula-hidden').val(field.latex()); }$('#math-input')[0]把 jQuery 对象换成原生 DOM 元素,内核要求传元素而不是 jQuery 对象,这是入门最容易踩的 API 差异。autoCommands是「边打字边转命令」的白名单——用户输入 sqrt 再按空格,会自动变成根号模板,不用非得用鼠标点按钮。handlers.edit在每次结构变化时触发,比如填完分子再把光标挪到分母时都会触发,但它不是每次按键都触发,所以直接在回调里同步 LaTeX 不会造成性能问题。
提示:初始化必须在元素已经存在于文档中之后调用。动态弹窗里初始化失败的问题,第 5 章专门有一条对应排查。
注意MathQuill.getInterface(2)的调用时机:mathquill.js 还没加载完就调用,这里会拿到 null,后续cmd、latex全部静默失效,控制台还不一定报错。我的习惯是在初始化函数入口先判断typeof MathQuill !== 'undefined',给一个明确的报错提示,而不是让用户面对一个点不进去的空 div。
3.3 工具栏按钮与命令注入:分式根式模板是怎么「点」出来的
工具栏的核心逻辑是把按钮上的命令注入到光标位置。这里必须用对 API,用错了屏幕上会出现一串源码。
// 事件委托:给 toolbar 绑一次,不管后面加多少按钮 $('#mq-toolbar').on('click', 'button[data-cmd]', function () { var cmd = $(this).attr('data-cmd'); // 读出来是 "\frac" 这种单反斜杠字符串 if (!cmd) return; field.cmd(cmd); // 在光标处注入并展开命令模板 field.focus(); // 焦点回到公式区,用户能直接输入内容 });为什么用cmd而不是write:field.write('\\frac{1}{2}')会把反斜杠和花括号当成普通字符逐字写进去,屏幕上出现的是带反斜杠的源码;cmd走的是命令解析通道,\frac会立即展开成分子分母两个空位,光标自动停在分子里。这一步就是「所见即所得」体验的核心——用户看到的永远是排版结果,不是源码。
工具栏按钮可以继续扩充,比如\alpha、\beta这类符号按钮,\begin{matrix}这类矩阵模板。只要内核白名单里支持的命令,>// 兜底:即使 edit 没触发过(比如初始化后直接点提交),这里也会强制同步 $('#question-form').on('submit', function () { syncLatex(); // 此时隐藏域里已经是标准 LaTeX,后端取 formula 字段即可 });
只在handlers.edit里同步有一个漏洞——用户打开编辑器没做任何修改就提交,edit 不会触发,隐藏域还是空的。所以 submit 时强制再同步一次,两次同步互不冲突:编辑时实时同步保证界面状态和隐藏域一致,提交时兜底保证万无一失。field.latex()在空公式时返回空字符串,后端要能区分「没填」和「填了{{}}残留」两种情况,存库前建议 strip 空白。
如果项目用的是 jQuery AJAX 提交而不是原生表单,逻辑也是一样的:在拼接请求参数的地方调用syncLatex(),再把formula字段塞进 payload。关键只有一条——取 LaTeX 的动作必须发生在序列化或取数之前,顺序反了提交上去的就是旧值。
4. 参数配置与界面调优:把公式编辑器调成顺手的样子
4.1 必看的配置项:autoCommands、handlers、括号限制
公式编辑器初始化时有一批配置项,大部分项目真正用得上的就这几个。用错了不会报错,但体验会明显不对。
| 配置项 | 作用 | 建议值 | 踩坑概率 |
|---|---|---|---|
| autoCommands | 键入白名单内的命令名后自动转成命令模板 | 'sqrt sum int pi theta alpha beta' | 中 |
| autoOperatorNames | 键入 sin、cos 这类运算名时保持正体 | 'sin cos tan log ln' | 中 |
| restrictMismatchedBrackets | 禁止左右括号交错闭合 | true | 低 |
| handlers.edit / enter / moveOut | 编辑、回车、焦点移出时的回调 | 按业务写 | 高 |
var field = MQ.MathField($('#math-input')[0], { autoCommands: 'sqrt sum int pi theta alpha beta', // 只能配内核白名单里有的命令 autoOperatorNames: 'sin cos tan log ln', // 让 sin 渲染成正体而不是斜体变量 restrictMismatchedBrackets: true, // 防止 ( 和 ] 交叉闭合 handlers: { edit: syncLatex, enter: function () { /* 回车行为见 5.3 的坑 */ } } });autoCommands不是字符串数组,是一串以空格分隔的命令名,而且只能配置内核白名单里已有的命令。不同版本的白名单有差异,我一般会先单独配置一条验证它生效,再批量加上去,避免配了一堆不生效还不知道。autoOperatorNames解决的是「sin 被渲染成斜体」的问题:默认把所有字母当变量,变量用斜体,但 sin 作为函数名应该是正体。restrictMismatchedBrackets开启后,用户在公式里写([)]这种交错括号会被拦截,对后端解析 LaTeX 友好很多,代价是输入行为更严格,如果业务场景允许非标准写法就不要开。
4.2 字号与行高:公式编辑部件的缩放入口只有一个
公式编辑器内部的排版尺寸不是随便调的。内核渲染分式时,分子、分母、根号内部都按 em 相对公式区根元素计算字号,所以缩放入口只有一个。
.mq-input { /* 唯一的缩放入口:内核内部所有尺寸都用 em 相对它计算 */ font-size: 16px; min-height: 40px; line-height: 2; } .mq-editable-field { border: 1px solid #d0d7de; border-radius: 6px; padding: 8px 12px; background: #fff; } .mq-editable-field.mq-focused { border-color: #1668dc; box-shadow: 0 0 0 3px rgba(22, 104, 220, 0.15); }要整体放大缩小公式,只改.mq-input的font-size一处即可。反过来,如果去对.mq-fraction这类内部 class 单独设字号,分子分母的相对排布会被撑乱,出现「分式横线不在正中」这种玄学错位,而且不同版本表现还不一样,排查起来非常折腾。聚焦态用内核自带的mq-focused类来控制,不要依赖:focus伪类——焦点实际落在内核内部的输入层上,外层 div 不一定能拿到 focus 状态。
4.3 公式区自动撑高:别让公式编辑出现内部滚动条
用固定高度装公式区是我见过最多的界面失误。公式超过一行时里面出现滚动条,使用者看不到自己刚输入的分子,体验直接崩掉。
.mq-input { overflow: hidden; /* 收起默认滚动条,公式多时让容器跟着长高 */ } .formula-editor { max-width: 720px; /* 外层限宽,防止公式无限横向铺开 */ }把高度交给内容自己撑,只保留min-height保证空状态下有一个可点击的区域。宽度限制放在外层容器而不是公式区上,这样长公式会自动换行而不是把页面撑破。如果项目里公式普遍很长,建议在弹窗类场景给公式区一个预设宽度,配合自动撑高,效果最自然。
4.4 编辑已有题目时回填 LaTeX:setter 的时机与合法性
列表页点「编辑」进入表单时,要把后端存的 LaTeX 回填进编辑器,顺序写反是高频失误。
// 后端存的是之前保存的 LaTeX 字符串 var savedLatex = $('#formula-hidden').val(); var field = MQ.MathField($('#math-input')[0], opts); // 初始化完成之后调用 setter,顺序不能反 if (savedLatex) { field.latex(savedLatex); }为什么要先初始化再回填:内核只有在初始化后才有结构树可反序列化,顺序反了等于往一个空壳上写数据,什么都不会发生。latex(str)遇到不合法的 LaTeX 时不会抛异常,而是能解析的部分解析、不能解析的直接丢弃——你打开编辑器会发现公式少了一半,但没有任何报错提示。所以后端输出到页面前最好对 LaTeX 做一次基础清洗:转义反斜杠、去除空行,至少别让脏字符串直接进 setter。这条坑经常在「编辑已录入题目」功能上翻车,录入时好好的,一编辑就丢内容,用户就会觉得系统把他的题改坏了。
5. 高发问题与避坑排查:让公式编辑器翻车的 5 个真实场景
5.1 提交后存进库的是空字符串:同步时序问题
现象:明明在编辑器里看到公式了,提交表单后数据库里formula字段是空串。
原因有两种,都是时序问题。一是只依赖handlers.edit同步隐藏域,但用户初始化后没编辑直接提交,edit 不触发;二是同步时机写在了表单序列化之后——jQuery 序列化时隐藏域还是旧值,之后才val()赋值,自然带不上去。
解决:把同步拆成两层,编辑时实时同步保持界面状态,提交事件里强制再同步一次。注意syncLatex()必须写在serialize()或 AJAX 取数之前,顺序反了一切白搭。我在第一次接这个组件时就栽在这条上,查了一下午才发现是顺序问题。
5.2 弹窗里公式区初始化失败:元素还没进文档就动手了
现象:用某个 jQuery 弹窗组件动态渲染表单,弹窗打开后公式区是个光秃秃的 div,点不进去;控制台偶尔报一个「0 height / 无法定位光标」之类的错误。
原因:初始化发生在弹窗内容还没插入文档、或者模板引擎还会二次重绘 DOM 的时机。内核初始化要做尺寸测量来排版,拿到 0 高度后内部结构直接放弃渲染。另一种情况是初始化成功了,弹窗关闭时 DOM 被整体移除,再次打开时同一个 div 被重建,之前的 field 对象指向的是已失效的元素。
解决:把初始化放到弹窗「显示完成」之后,这个事件在不同弹窗组件里叫法不一样,常见的是 shown。再加一个标记变量保证只初始化一次:
// shown 是弹窗显示完成后的回调,以你用的组件文档为准 $('#dialog').on('shown', function () { if (!dialog.__mathField) { dialog.__mathField = MQ.MathField($('#dialog-math')[0], opts); } });标记变量是防重复初始化用的——同一个 div 被初始化两次,内核会再包一层结构,表现是公式区出现双层光标、输入一个字符显示两个。生产环境把标记挂在弹窗实例对象上,别挂全局变量。
5.3 回车键把整个表单提交了
现象:在公式区按回车,页面刷新或者直接跳转,刚输入的公式没存下来。
原因:公式输入层底层是内容可编辑的输入控件,回车键默认行为会冒泡触发表单提交;内核提供的enter回调默认不阻止这个冒泡。于是「回车想在公式里换行」变成了「回车把表单提交了」。
解决:在handlers.enter里接管回车行为,把焦点移交到下一个输入项:
handlers: { enter: function () { $('#next-field').focus(); return false; // 部分版本支持返回 false 阻止默认提交 } }公式本身一般不需要换行——分式、矩阵这类结构都是命令生成的,回车在公式编辑里的语义本来就是「结束编辑或跳到下一项」。别想着去实现公式内换行,直接转移焦点才是最符合使用者习惯的做法。不同版本对return false的支持有差异,升级内核版本后先在一个 demo 页验证回车行为,不要直接上生产。
5.4 中文混排丢字符:数学字体里没有汉字字形
现象:输入「物体从高度 h 落下」,汉字部分要么消失、要么变成方块;latex()导出的字符串里中文被吞掉或变成乱码。
原因:公式区的默认字体是数学字体,里面没有 CJK 汉字字形。输入的汉字被当成数学符号参与解析,解析不了就直接丢弃,屏幕上剩下的只有字母和符号。
解决:把连续中文片段包进\text{},让内核按普通文本渲染。编辑完成后做一次保护性替换:
function protectCJK(latex) { return latex.replace(/([\u4e00-\u9fa5]+)/g, '\\text{$1}'); }这个正则是按连续中文分组的,「h米」这种中文紧贴字母的情况会把「米」单独包一层\text{米},渲染没问题但 LaTeX 源码语义偏怪。更专业的做法是在产品交互上解决:需要混排中文时,先让用户插入一个文本块,再在文本块里输入中文。小项目可以用正则兜底,正式产品要在输入端引导。
5.5 报错$(...).mathquill is not a function:jQuery 多实例冲突
现象:页面其他功能正常,公式编辑器死活初始化不了,控制台报这句错。
原因:老页面常见。运营系统先加载了业务用的一套 jQuery,某个第三方组件又自带了另一套 jQuery 直接覆盖全局;插件脚本挂载在它引入那一刻的 jQuery 实例上,而业务代码拿到的是另一个实例,$.fn.mathquill对后者不存在。
解决:放弃 jQuery 插件调用方式,改用静态接口初始化,绕开实例归属问题:
if (typeof MathQuill !== 'undefined') { var MQ = MathQuill.getInterface(2); // 静态接口不依赖 $.fn.mathquill,多 jQuery 环境下也能用 var field = MQ.MathField($('#math-input')[0], opts); } else { console.error('mathquill 内核未加载,检查脚本引入顺序'); }MQ.MathField走的是内核静态命名空间,只要MathQuill全局对象在就能用,不关心它内部挂在哪个 jQuery 实例上。这是多 jQuery 环境下的最后手段;如果页面能清理,优先把两套 jQuery 合并成一套,因为公式编辑器只是其一,页面其他地方的插件可能还有类似的挂载冲突。
6. 进阶用法:只读回显、快捷键与组件化封装
6.1 用静态接口做只读公式展示
试卷预览、批改回显这些场景不需要再次编辑公式,用可编辑内核太重。静态接口只渲染不接收输入:
var answerView = MQ.StaticMath($('#mq-answer')[0]); answerView.latex(savedLatex || '');静态模式和编辑模式共享同一套排版逻辑,保证「录入时看到的样子」和「回显的样子」一致。注意静态实例同样要在元素挂载后再初始化。
注意:静态回显和动态编辑不要混用同一个元素,先 static 再 editable 会造成残留结构。
6.2 给高频模板配快捷键
鼠标点工具栏对高频操作还是慢。把快捷键绑在容器上,靠事件冒泡捕获内核输入层的按键:
$('#formula-box').on('keydown', function (e) { if (!(e.ctrlKey || e.metaKey)) return; if (e.key === '1') { field.cmd('\\frac'); e.preventDefault(); } if (e.key === '2') { field.cmd('\\sqrt'); e.preventDefault(); } });避开浏览器保留快捷键,用带数字的组合键最安全。我一般会在工具栏按钮标题里顺带标注快捷键,比如「分数 (Ctrl+1)」,让使用者自己发现这个快捷方式。
6.3 把整个逻辑收敛成一个初始化函数
项目里有多个表单页要用公式输入,把前面的代码收成一个函数,每个页面三行接入:
function initMathEditor(container, opts) { var field = MQ.MathField(container.querySelector('.mq-input'), { autoCommands: opts.autoCommands || 'sqrt sum int pi', handlers: { edit: function () { opts.onChange && opts.onChange(field.latex()); } } }); return { getLatex: function () { return field.latex(); }, setLatex: function (s) { field.latex(s || ''); }, focus: function () { field.focus(); } }; }这套封装我一直在用,原因是它把「取公式」的入口收敛成getLatex()一个方法,业务代码不需要知道内核 API 长什么样。我最早给一个题库系统接公式输入时,就是栽在「只在提交时同步一次」这个习惯上,后来改成实时同步加提交兜底的双保险,再没因为丢公式被追问过。做公式编辑器这类偏门组件,把同步时序和初始化时机管住,基本就避开了八成问题。希望帮到你。
本文还有配套的精品资源,点击获取