简介:这是一份基于JavaScript与HTML5的网页公式编辑器源码包,适合前端学习者、在线教育开发者或科研人员快速搭建数学公式输入与绘图功能。编辑器支持LaTeX/MathML公式解析、函数表达式输入及图形绘制,并涉及事件监听、DOM交互、跨浏览器兼容与性能优化等常见Web开发知识点,整体代码量精简,便于阅读和二次改造。压缩包内共2个文件,包含1个JavaScript逻辑文件与1个HTML结构页面,包体仅9KB,轻量易部署,直接打开即可查看编辑与渲染效果。该资源发布以来已有1310人学习下载,适合想通过实际案例掌握公式解析、Canvas/SVG绘图及前端组件封装的人群。通过阅读代码,可以了解KaTeX/MathJax类库的替代实现思路、函数图像实时绘制流程,以及在不依赖重型框架情况下完成公式编辑交互的可行方案,对于正在做在线作业系统、数学工具站或需要公式输入场景的开发者是一份实用的小型参考样本。
1. javascript公式编辑器:在浏览器里写公式这件事,比你想的更绕
给一个在线题库或教务系统加公式录入功能时,产品经理一句“就一个公式编辑器需求”,背后其实拆成三层:“公式怎么输进去”“输入之后怎么渲染出来”“存下来之后别人怎么再编辑”。标题里的 javascript公式编辑器,并不是某个现成库的名字,而是用 JavaScript 在网页里做“可录入、可预览、可回显”的公式输入能力。这类需求在习题批改、科研协作、低代码表单里反复出现。难点不在“渲染一个公式”,而在于让不熟悉 LaTeX 的用户也能顺畅录入,让熟悉 LaTeX 的用户不被打断,同时保证最终落库的是干净、可逆的源数据。这三件事同时做到,才叫一个能上线的公式编辑器。
2. 公式编辑器的第一个岔路口:渲染引擎与输入形态怎么选
2.1 MathJax 与 KaTeX:两个主流渲染引擎的取舍
公式编辑器的地基是渲染引擎。当前主流是 MathJax 与 KaTeX 两套。落地前先分清它们的性格,比急着写代码重要。
KaTeX 的核心优势是“快”。它是纯前端渲染,输出的 HTML/CSS 比较轻,页面里一次性渲染几百条公式也不吃力。KaTeX 由 TeX 排版系统衍生,支持 LaTeX 语法的大部分常用子集,像上下标、分式、根式、求和积分、矩阵、多行公式对齐环境都能处理。代价是“容错差”:遇到不合法的表达式直接抛错,不会像 MathJax 那样“渲染出一个近似结果”。这个特点其实可以反过来用——把 KaTeX 渲染失败当成用户输入的合法校验器。另外 KaTeX 的字体和渲染样式相对固定,做精细排版定制比较费劲。
MathJax 的优势是“全”和“稳”。它在底层做了大量兼容,遇到未识别命令、括号不配对、字体缺失等情况时,会尽力给一个可读的输出,不直接崩掉。MathJax 的启动和渲染速度比 KaTeX 慢不少,初次加载字体也多,在页面里动态插入公式时会有可感知的延迟。如果业务里公式以“整篇文档排版”为主,用户对毫秒级反馈不敏感,MathJax 更合适;如果是表单里逐字符敲公式、需要预览立刻跟上,KaTeX 是更省心的选择。
还有一层要考虑:MathJax 可以把一个公式渲染成 MathML,这对对接无障碍阅读器和结构化文档有帮助。KaTeX 也保留了解析后的源码,但生态里更多是“渲染 + 校验”的玩法。我记得两套引擎都还提供“自动扫描页面中的美元符号/括号”的 auto-render 扩展,但它更适合渲染已存在的内容,不适合做编辑器实时预览。
2.2 可视化输入还是源码输入:按目标用户分岔
渲染引擎只是输出层,真正的体验分岔在“用户怎么写公式”。两类方式最常见。
第一类是源码输入:用户直接在文本框里写 LaTeX 命令,输入的同时旁边给实时预览。这种方式对熟悉 LaTeX 的理工科用户效率极高,实现成本也最低。它对不熟悉 LaTeX 的人不友好——一个\frac{1}{2}可能劝退文科出身的运营同学。
第二类是可视化输入:界面上摆着一排按钮(上下标、分式、根号、求和、希腊字母),点按钮往光标处插入命令模板,用户只填“空位”里的内容。更高阶的做法是像 MathQuill 那样,把公式渲染和光标定位合在一起,用户直接“所见即所得”地操作公式结构,完全不需要认识 LaTeX 命令。但这种方案的实现复杂度高出几个量级,涉及自定义光标管理、组合状态、DOM 节点分类等,做起来远不止一个组件,是一个完整的编辑内核。
我的判断标准很简单:面向教师、科研人员、学生答题场景,源码输入 + 实时预览就够,配合工具栏按钮能覆盖 80% 的录入效率需求;面向行政表单、非专业录入场景,才值得上可视化编辑。大多数“javascript公式编辑器”的搜索诉求,落在前者。先把这条主线做好,再决定要不要加更多层。
2.3 选型决策表:五个维度定方向
| 维度 | KaTeX 优先的情况 | MathJax 优先的情况 |
|---|---|---|
| 实时预览 | 逐字符输入,预览要零延迟 | 整篇文档渲染,延迟感知弱 |
| 出错策略 | 渲染失败即报错,可复用为校验 | 尽力渲染,不轻易打断用户 |
| 质量要求 | 常用 LaTeX 子集够用 | 需要边界语法、MathML、无障碍 |
| 部署负载 | 前端静态文件即可 | 字体多,要考虑首次加载耗时 |
| 定制空间 | 样式相对固定 | 排版参数可调,生态工具多 |
如果实在摇摆,我一般先按 KaTeX 走。原因是:公式编辑器在绝大多数场景里的瓶颈是“输入体验”,而输入体验对实时渲染速度最敏感。KaTeX 快、轻、能校验,够用。等真遇到“必须渲染某种官方不支持命令”或“要对接 MathML 输出”的业务,再局部替换成 MathJax,渲染接口并不复杂,切换成本是可控的。
3. 用 textarea 加 KaTeX 跑通最小公式编辑器:可复制的完整代码
3.1 最小实现:源码与预览双栏
不走工程脚手架,先用一个单文件 HTML 把链路跑通。这就是一个“最小可用公式编辑器”:左侧写 LaTeX 源码,右侧实时渲染,出错时把错误信息显示出来。代码是完整的,可以直接存成 html 打开看效果。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>最小公式编辑器</title> <!-- 注意:务必要同时引入 katex 的样式与脚本,缺样式会渲染成纯文本 --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css"> <script src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js"></script> <style> body { font-family: sans-serif; max-width: 900px; margin: 40px auto; } .editor-row { display: flex; gap: 16px; } .editor-pane { width: 50%; } textarea { width: 100%; height: 200px; box-sizing: border-box; font-family: "Courier New", monospace; font-size: 14px; padding: 12px; } #preview { width: 100%; height: 200px; box-sizing: border-box; border: 1px solid #ddd; background: #fff; padding: 12px; overflow: auto; font-size: 18px; } #errorMsg { color: #c00; margin-top: 8px; min-height: 20px; font-size: 14px; } .toolbar { margin-bottom: 8px; display: flex; gap: 6px; flex-wrap: wrap; } .toolbar button { padding: 6px 10px; cursor: pointer; } </style> </head> <body> <h3>LaTeX 源码 / 实时预览</h3> <div class="toolbar"> <!-- 工具栏按钮:把命令模板插入光标处,下面用 textarea 版本做示例 --> <button>// 以 CodeMirror 6 为例,示意编辑器实例与选区工具函数 import { EditorView, basicSetup } from 'codemirror'; import { EditorState } from '@codemirror/state'; // 自定义一个极简 LaTeX 高亮:命令给蓝色,注释给灰色 const latexHighlight = EditorView.theme({ '.cm-keyword': { color: '#0000cc' }, '.cm-comment': { color: '#999' } }); const view = new EditorView({ parent: document.getElementById('editor-mount'), state: EditorState.create({ doc: '\\frac{1}{2} + \\sqrt{16}', extensions: [ basicSetup, latexHighlight, EditorView.updateListener.of((update) => { if (update.docChanged) { // 防抖渲染逻辑在这里触发 schedulePreview(update.state.doc.toString()); } }) ] }) }); // 取编辑器当前总内容 function getLatex() { return view.state.doc.toString(); } // 用指定文本替换当前选区 function replaceSelection(text) { view.dispatch({ changes: { from: view.state.selection.main.from, to: view.state.selection.main.to, insert: text } }); view.focus(); }逻辑说明:updateListener是唯一的“内容变化”入口,所有对编辑器内容的修改,包括撤销、粘贴、程序化替换,都会触发它,比只监听键盘输入可靠。docChanged判断是为了避免光标移动等非内容变化也触发重渲染。replaceSelection里的from和to如果相等,就等价于在光标处插入内容。
4.2 工具栏插入的完整逻辑:光标、选区与转义
工具栏在多数字产品里都会保留。用 CodeMirror 后,插入逻辑从 textarea 的selectionStart/End变成上面这种dispatch + changes。有一个坑值得重点说:LaTeX 里的反斜杠在 JavaScript 字符串里的写法。
// 错误写法:'\frac{}{}' 中的 \f 会被当成换页符,得到乱码源串 const tmp1 = '\frac{}{}'; // 正确写法:写成双反斜杠,避免 JS 字符串转义吃掉反斜杠 const tmp2 = '\\frac{}{}'; // 模板字符串同样要小心:${} 会被当成插值 const tmp3 = `\\frac{${'1'}}{${\'2\'}}`; // 注意内层的双反斜杠常见做法是把工具栏每个按钮的命令模板维护在一个配置表里,统一用双反斜杠写,插入前再做一次断言检查:if (template.indexOf('\\') === -1) throw new Error('模板缺少反斜杠')。这层检查虽然简单,但能拦下一类极其隐蔽的问题:模板字符串里写'\frac'后,页面渲染总是不对,但源码看起来又很正常,控制台也不报错,最后发现是字符串转义把命令破坏了。
工具栏插入时,另一个加分项是“选中即包绕”。用户用鼠标选中了1 + 2,点“分式”按钮,期望得到的是\frac{1 + 2}{},而不是插入一个全新的空分式。要支持这个,替换逻辑需要读取选区文本:
function wrapWithCommand(command, argCount) { const main = view.state.selection.main; const selected = view.state.doc.sliceString(main.from, main.to) || ''; let insertText = command; // 把光标放到第一个空参数位(实现从略,示意思路) view.dispatch({ changes: { from: main.from, to: main.to, insert: insertText } }); }这个“包裹选区”的能力,是把公式编辑器从“能用”做到“好用”的关键一步。按这个思路,\sqrt{}、\frac{}{}、\int_{}^{}这类带参数的模板都能和用户操作对应起来。
4.3 可交付的编辑器还要有:状态提示、换行与模块化
工具栏和快捷键之外,三个产品化细节别省。
状态提示:编辑器下方保留状态区,展示“当前公式合法/包含 X 个未闭合大括号/渲染失败”等信息。上面 3.1 里的errorMsg就是这个角色。这里不仅要显示错误,最好给出错误位置。KaTeX 的解析错误对象里有position属性,指向出错字符在源码中的偏移量,可以用它把光标定位到出错处。
换行策略:编辑器宽度有限,源码输入必然需要换行。直接用\\和 LaTeX 的行宽语义绑定会干扰用户。常见做法是:编辑器内允许肉眼换行(硬换行),但渲染前把行首行尾的空白trim掉,再把行间换行合并成空格,只把用户明确输入的\\当作 LaTeX 换行命令。这个策略我在前面 3.2 提过,在 CodeMirror 版本里更好实现,因为doc.toString()能精确拿到整篇源码。
模块化:把“编辑区、工具栏、预览区、状态区”拆成独立组件,对外暴露getLatex()、setLatex(text)、previewSourceChanged回调。入库前业务层只需调getLatex()拿源码串;编辑时预览可以由组件内部完成半自动。这样公式编辑器就是一个纯前端组件,与后端存储结构解耦。我曾见过把getLatex()写在业务页面里、每处都自己拼渲染参数的写法,换一个页面就要改一遍,后来统一收敛到组件内,才消停。
5. 公式编辑器避坑记录:从渲染空白到粘贴乱码
5.1 渲染与显示区:三个高频现象
现象一:公式区域一片空白,控制台没有任何报错。原因多数不是代码逻辑,而是 KaTeX 的 CSS 没有加载,或字体文件跨域被浏览器拦截。KaTeX 渲染输出的结构依赖自带字体和样式,样式缺失时,渲染结果里有字符但被 CSS 隐藏或挤压成不可见形态。解决:检查 Network 面板里katex.min.css及其引用的 woff2/ttf 是否都返回 200;生产环境如果用了 CDN,要确认字体文件路径没有被打包工具改坏;本地部署时把katex.min.css与字体文件放在同一目录,用相对路径引入。
现象二:同一个公式在本地正常,在客户内网环境渲染错位。原因多为内网浏览器版本过旧或字体渲染差异。KaTeX 对较老浏览器的支持有边界,部分单位内网还是旧版 Chromium 内核。解决:先用兼容性表格确认团队目标浏览器范围;如果内网环境确实旧,另一个方案是回到 MathJax 的 SVG 渲染,output: 'svg',把公式转成 SVG 节点,彻底绕开字体加载。代价是渲染速度下降,但对内网系统通常可接受。
现象三:输入长公式时预览越拖越卡,CPU 占用飙升。原因常是每次input都全量渲染,且公式里含大括号嵌套、多行环境时,KaTeX 解析成本线性上升。解决:防抖延迟拉高到 250~300ms;在渲染前对比“当前源码”与“上次渲染源码”,一致就跳过;最激进的方式是把大公式拆成多个inline片段分别渲染,或者切到 MathJax 的延迟渲染策略。多数场景防抖加比对就够了。
5.2 输入与数据区:两个更深层的坑
现象四:前端预览完全正常,公式发给后端后乱了或解析失败。原因是前端源码在 JS 字符串处理过程中反斜杠被吞,或者后端接手时对 LaTeX 源串做了未转义处理。这是最隐蔽的一类问题。解决:前端统一使用双反斜杠模板,并在getLatex()出口做一次校验;后端拿到字符串后,打印原始字节看反斜杠数量是否和前端一致;如果前后端之间走 JSON,还要确认 JSON 序列化没有二次转义。库表里存公式源串,永远不要只存渲染后的 HTML,HTML 不可逆且会膨胀。
现象五:用户从 Word 里复制公式(Office MathML 或 MathType 生成的富文本)粘贴进编辑器,得到一坨乱码。原因是粘贴时浏览器默认把剪贴板中的text/html交给光标处,包含大量 XML 标签和私有标记,textarea 或 CodeMirror 会把这些当纯文本塞进去。解决:拦截编辑器区域的paste事件,用clipboardData.getData('text/plain')只取纯文本写入;如果业务上确实需要支持 Word 公式转换,取到纯文本后先做一轮清洗,把<!--、<等 XML 痕迹剔除,或者走一个独立的“Word 公式转 LaTeX”转换工具。最保守的底线是:粘贴进来的任何内容,必须先校验是合法 LaTeX,再允许入库。
6. 进阶:离屏渲染、图片导出与公式三段校验法
6.1 离屏渲染与导出图片:集成到文档导出链路
公式编辑器在在线文档、报告生成、题库导出场景里的最终产物,不只是“页面里能看”,还要能落到 PDF 或 Word 里。常见做法是离屏渲染:新建一个不在视口中的 DOM 容器,把同一个 LaTeX 源码渲染进去,然后借助浏览器能力导出图片或 SVG。
// 离屏渲染:把公式转成 SVG 字符串,供后续导出或提交后端 function latexToSvg(latex, isBlock = true) { const temp = document.createElement('div'); temp.style.position = 'absolute'; temp.style.left = '-9999px'; document.body.appendChild(temp); try { katex.render(latex, temp, { displayMode: isBlock, output: 'html', // 若引擎支持,可换 'mathml' 配合结构化需求 throwOnError: true }); return temp.querySelector('svg') ? temp.innerHTML : ''; } finally { document.body.removeChild(temp); } }这段逻辑说明:把容器移到屏幕外,避免用户看到渲染瞬间的闪烁;throwOnError: true用于保证不合格公式能抛错被上层捕获。图片导出时可以拿这个 SVG 字符串去绘制<img>,也可以交给服务端转成 PNG。离屏渲染不占用主预览区,导出和编辑是两个互不干扰的线程过程。
6.2 公式三段校验法:渲染只是第一层
公式经过编辑器后,数据质量决定后续能不能复用。我习惯在项目里固定一条三段校验流程:第一步,渲染校验,前端用 KaTeX 渲染源码,抛错即拦截;第二步,结构校验,用解析器检查括号配对、命令结尾、未知环境这几项,拦截一些“能渲染出来但语义不对”的输入;第三步,入库后回读校验,从数据库读出源串再渲染一次,确认存取过程没有转义损耗。
| 校验层 | 位置 | 手段 | 目标 |
|---|---|---|---|
| 渲染校验 | 前端预览 | KaTeX / MathJax 渲染抛错 | 拦截写不了的表达式 |
| 结构校验 | 前端/服务端 | 括号配对、命令白名单 | 拦截能渲染但语义错的输入 |
| 回读校验 | 后端 | 重读库表再渲染 | 确认序列化无损耗 |
这个三层校验做完,公式数据的可信度才稳。图片导出、回声测试、无障碍输出这些能力,都可以间接复用这套链路。运行环境和集成方式会变——从单页表单到富文本编辑器再到大文档系统——但“输入源码、实时渲染、严格校验、按需导出”这条主线不变。我做一个公式编辑器时最后都会保留这四件套,后面接什么场景都顺。前几次做这类需求时,我只顾着渲染好不好看,后来被线上数据乱码和导出错版教了一课,才把校验工序提到这个位置。这套流程运行一段时间后,你可以明显感到维护成本降下来了——毕竟公式出错的来源大多就那么几个。希望帮到你。
本文还有配套的精品资源,点击获取