纯JavaScript实现半所见即所得公式编辑器:LaTeX存储与KaTeX渲染
简介这是一份基于JavaScript与HTML5的网页公式编辑器源码包适合前端学习者、在线教育工具开发者以及需要在网页中嵌入数学公式编辑功能的科研或教学场景。压缩包共2个文件包含1个js逻辑文件与1个html页面整体仅9KB结构精简便于快速阅读和二次开发。编辑器支持数学公式的输入、解析与渲染并集成了函数编辑与图形绘制能力涉及LaTeX/MathML解析、Canvas/SVG绘图、事件监听与跨浏览器兼容性处理等关键知识点。已有1309人学习下载。通过这份源码可以直观理解公式输入到可视化输出的完整流程掌握自研公式编辑器的核心实现路径还能借鉴前端交互设计与性能优化的落地方法适合以此为基础扩展更复杂的在线公式编辑方案。1. 整体设计先想清楚你要做哪种公式编辑器真正着手写一个 JavaScript 公式编辑器很多人第一反应是“这有什么难的不就是个能打公式的输入框嘛”。等真做起来才发现难点根本不在排版而在数据怎么存、光标怎么管、渲染怎么刷新。更麻烦的是不同用户对“公式编辑器”的预期完全不一样有人只想在网页里显示一段 LaTeX有人要能像 Word 那样点在公式上就能改还有人要兼顾导师发来的 .docx 里的公式要求能复制过来不报错。我这次做的方案定位很明确用纯 JavaScript 实现一个“半所见即所得”的公式编辑器前端负责工具栏插入和实时渲染内部用 LaTeX 字符串做数据存储配合 KaTeX 做渲染核心代码不依赖框架方便在任何项目里搬。如果你也是刚接到这类需求先别急着抄代码下面这个需求分级能帮你少走一半弯路。1.1 需求分级从“能写公式”到“像Word一样编辑”我把公式编辑器按实现成本分成三档每一档的技术路径完全不同第一档源码 预览。页面放一个 textarea 输入 LaTeX旁边渲染出公式。这是成本最低的方案一个下午就能做完。优点是实现简单、数据天然是文本、不涉及光标操作。缺点是用户得自己懂 LaTeX 语法别说普通用户很多研究生写矩阵照样会漏掉\\换行符。第二档工具栏辅助 实时渲染。在源码输入框上方加一排按钮用户点“分数”按钮就自动插入\frac{}{}并让光标停在分子位置。输入过程仍然以 LaTeX 源码为主但大部分语法细节由工具兜底。我做的就是这一档成本和收益比较平衡。第三档完全所见即所得。用 contenteditable 或 Canvas 直接绘制公式用户在界面上看到什么就编辑什么光标可以点在任意位置。这一档在视觉和体验上是最接近 Word 公式编辑器的但实现成本非常高一是公式的光标定位需要自己算坐标二是 LaTeX 源码和界面元素之间要保持双向同步稍有不慎就会出现“界面显示正常、导出源码乱掉”的问题。我的建议是除非你明确要做一个独立产品否则别从第三档开始。1.2 渲染层选型KaTeX还是MathJax公式渲染没什么悬念业内基本就是 KaTeX 和 MathJax 两家。我选了 KaTeX理由是它渲染快主流程同步执行对需要频繁交互的编辑器场景更友好。MathJax 对复杂公式的兼容性略强但渲染是异步的在编辑器里会带来渲染回调、二次排版这些问题调起来比较烦。可以看一个直观的对比对比项KaTeXMathJax渲染速度快适合高频刷新偏慢复杂公式更明显支持命令范围覆盖绝大多数常用公式覆盖更全含少量宏包命令二次渲染同步完成易控制异步排版需要回调处理体积约 300KB 左右更大加载时间更长常用场景编辑器、需要频繁更新渲染静态文档、技术博客我在项目里最终用了 KaTeX 自定义工具栏的组合。如果你后续要支持特别复杂的数学环境可以再用 MathJax 做兜底渲染两者并不冲突编辑器内用 KaTeX 保证交互流畅导出或展示时用 MathJax 保证兼容性。2. 核心数据结构与渲染管线公式编辑器最核心的决策不是用哪个库而是数据怎么存。数据格式决定了你用起来顺不顺手也决定了后续能不能接入文档系统、能不能导出到 Word。2.1 内部用LaTeX字符串但界面不要裸展示我见过不少项目把公式存成 JSON 树节点有 type、value、children设计得挺复杂。这种结构在做“所见即所得”编辑器时需要但对“工具栏插入 源码编辑”的半所见即所得方案来说属于过度设计。JSON 树最大的问题是 debug 成本高公式一旦复杂你很难一眼看出树哪里拼错了而且和外部交互时要先序列化再解析用户想复制一段代码还得靠后端转换。最实用的做法是内部直接用 LaTeX 字符串当唯一数据源。用户在输入框里敲的是 LaTeX工具栏按钮插入的也是 LaTeX存到后端还是 LaTeX。渲染层只负责把字符串渲染成公式不参与数据存储。这样做的最大好处是透明、可移植接到任何地方都能直接用。注意这里的“输入框”最好用 textarea 而不是 input因为公式源码通常很长input 单行会很难看而且 input 的默认键盘事件在移动端设备上有各种兼容问题。2.2 编辑防抖、渲染刷新与同步策略公式编辑器里最容易出现的交互故障是“用户输入一个字符整屏公式闪一下”。如果每个按键都触发渲染KaTeX 也会撑不住尤其是大文档里多个公式同时存在时。解决方式是引入防抖监听输入事件150 毫秒内没有新的输入才触发渲染。我自己的实现里把这部分做成了一个较完整的流程textarea 监听 input 事件 → 更新内部 LaTeX 字符串 → 防抖 150ms → 调用 KaTeX 渲染到旁边的预览区。渲染前先清空预览区旧内容避免上次渲染的残留和这次的混在一起。这里有一个容易被忽略的点当用户从工具栏选择插入模板时应该立即同步触发渲染不要走防抖逻辑。因为插入模板是明确的用户意图而且模板本身是完整的公式片段不存在“输入到一半”的情况立即渲染的体验会更跟手。可以给插入和打字分别设计刷新策略一个是强制渲染一个是防抖渲染。3. 实操过程与核心环节实现下面进入重头戏我从实际代码里抽一个精简版本出来把关键部分讲透。这个版本虽然简单但骨架完整你照着写完就能在浏览器里跑起来。3.1 基础HTML结构页面上只需要三个核心部分工具栏、LaTeX 源码输入区、公式预览区。我把它们放在一个容器里形成上中下结构。div classformula-editor div classtoolbar button typebutton>class FormulaEditor { constructor(textareaId, previewId) { this.textarea document.getElementById(textareaId); this.preview document.getElementById(previewId); this.isRendering false; this.pendingRenderId null; this.bindEvents(); } bindEvents() { // 将文本域输入、键盘事件、工具栏点击聚合到统一处理入口 this.textarea.addEventListener(input, () this.scheduleRender()); document.querySelectorAll(.toolbar button).forEach(btn { btn.addEventListener(click, () this.insertTemplate(btn.dataset.latex)); }); } // 插入 LaTeX 模板并把光标移动到第一个空的花括号里 insertTemplate(latex) { const start this.textarea.selectionStart; const end this.textarea.selectionEnd; const current this.textarea.value; // 把模板替换选区并计算插入后光标应该停留的位置 const nextValue current.slice(0, start) latex current.slice(end); this.textarea.value nextValue; let cursorPos start; const placeholderIndex nextValue.indexOf({}, start); if (placeholderIndex ! -1) { cursorPos placeholderIndex 1; } this.textarea.focus(); this.textarea.setSelectionRange(cursorPos, cursorPos); this.render(); } scheduleRender() { if (this.pendingRenderId) { clearTimeout(this.pendingRenderId); } this.pendingRenderId setTimeout(() this.render(), 150); } render() { const latex this.textarea.value; this.preview.innerHTML ; if (!latex.trim()) return; try { katex.render(latex, this.preview, { displayMode: true, throwOnError: false }); } catch (err) { this.preview.innerHTML 公式解析出错 err.message; } } } // 页面加载完初始化 const editor new FormulaEditor(latexInput, formulaPreview);插入模板后通过setSelectionRange(cursorPos, cursorPos)把光标放到\frac{}{}的第一个空括号里这样用户可以直接输入分子输完 Tab 或手动点一下就能跳到分母。数据同步上就一条原则textarea 永远是唯一真实数据源预览区只是它的渲染结果永远不要反向操作。补充一个小技巧模板里特意用{}而不是{ }就是为了让程序能通过查找字符串快速定位占位符。如果你后续要做更高级的“跳转到下一个占位符”功能也可以基于这个思路来实现。3.3 连等号换行Word用户最常问的问题热搜词里有“word公式编辑器怎么输入换行的连等号”这个问题在网页端同样常见。很多人想要的效果是第一行a b c第二行 d e两个等号上下对齐。在 LaTeX 里想实现这个效果核心是用aligned环境并把等号前后的结构用标记对齐位置。示例代码如下\begin{aligned} a b c \\ d e \end{aligned}这段公式的意思是aligned环境按对齐每个所在列是对齐锚点第一行的等号跟第二行的等号在同一个垂直位置\\负责换行。KaTeX 支持aligned环境所以可以直接用在编辑器里。我在工具栏里专门加了一个“连等号”按钮点击后自动插入上面的模板并把光标定位到第一行等号右边这样用户就不需要去记aligned语法。实际使用下来很多原来以为“网页做不了公式换行对齐”的同事用了这个按钮后都觉得很直接因为它跟 Word 里“在等号处对齐”的交互逻辑是天然对应的。// 连等号模板 const alignedTemplate \\begin{aligned}\na \\\\\n \n\\end{aligned}; // 插入后光标定位到第一个空位也就是第一行等号右边3.4 工具栏交互细节插入、替换与撤销还有一个常见需求是“选中一段源码后点击按钮希望新模板替换选中内容”。上面insertTemplate方法已经处理了用selectionStart和selectionEnd把选中区截下来用模板替换掉这段选区再重新定位光标。这个逻辑虽然简单但能避免用户“先删再插”的冗余操作。撤销操作要额外说明如果用程序修改了 textarea 的 value浏览器默认的CtrlZ往往是失效的或者撤销会直接跳到输入最开始的地方。原因是程序性赋值不会进入浏览器的 undo 栈。这个问题的标准解法是手动维护一个历史栈每一次插入/替换前把当前值压栈监听 CtrlZ 时弹出栈并恢复。这个功能的实现不难但代码量不小建议如果产品还没有明确说要“撤销”可以先不急着做因为一旦做错会让用户更恼火。4. 常见报错与排查技巧实录写公式编辑器时我踩过的坑还真不少而且基本都不是公式语法问题全是 JavaScript 和浏览器行为方面的。下面把几个测试中也频繁出现的问题列出来按出现频率排序。4.1 按钮点了没反应或者页面跳了一下如果你用button标签并且没有指定typebutton在表单里它默认就是typesubmit点击后会尝试提交表单导致页面刷新看起来就是“点了没反应但是页面闪了一下”。这个问题非常隐蔽尤其是页面结构里只有一个按钮时不容易注意。另外如果按钮标签是a hrefjavascript:void(0)也有可能因为某种原因导致点击事件没绑定成功。void(0)本身是让链接不跳转但如果你在 onclick 里写了return false又同时绑定了 addEventListener两者的执行顺序不同某些浏览器会出现事件未按预期触发的现象。我的建议是表单场景里统一用button typebutton不要依赖javascript:void(0)。排查方法很简单在按钮点击回调第一行加一个console.log看有没有打出来。如果打出来了但公式没变再检查 template 字符串是否带上了多余的转义符如果没打出来就检查事件绑定或按钮类型。4.2 KaTeX渲染残留、重复渲染导致公式闪烁KaTeX 渲染前如果预览区已经有旧内容直接调用katex.render会把新内容追加进去造成公式叠在一起。所以渲染函数第一行一定要先preview.innerHTML 。这个坑挺常见的我见过不少刚上手的人在这上面卡很久。还有一个跟重复渲染相关的场景如果你的页面里同一个公式要渲染多次比如列表循环渲染KaTeX 内部会有缓存首次渲染可能稍慢第二次之后会快很多但要注意不能渲染同一个 DOM 节点多次插入的副本否则段落顺序可能会错乱。建议在循环里对每个公式用独立的容器。4.3 MathJax和KaTeX混用时的冲突有些人为了兜底会在编辑器里同时引入 MathJax 和 KaTeX。结果发现页面里公式要么重复渲染、要么两边样式叠加导致排版乱。原因是两个库都在页面加载时自动扫描文档一旦发现\(...\)或$...$就会重复处理。如果你确实需要两者共存最好把编辑器的渲染区域设为 KaTeX 专属用 class 隔离并在引入 MathJax 时配置skipHtmlTags和processHtmlClass让 MathJax 忽略编辑器区域。更稳妥的替代方案是用 MathJax 做静态展示编辑器内始终用 KaTeX互不干扰。4.4 光标跳位插入模板后光标不在预期位置插入模板后我发现光标有时候会跳到整个字符串末尾而不是模板内部。典型原因是setSelectionRange在执行前textarea 还没有重新获得焦点。正确顺序是先赋值 → 再textarea.focus()→ 再设置光标位置。上面代码里就是这个顺序。另外在移动端 Safari 上setSelectionRange有时会因为软键盘弹出导致位置计算偏移。这个问题的规避方法比较粗糙但有效在focus()之后用requestAnimationFrame延迟一帧设置光标位置等待浏览器完成布局调整。4.5 本地打开HTML时公式不渲染如果你直接用浏览器打开本地 HTML 文件会在控制台看到 CORS 相关的报错大概率是因为 KaTeX 的字体文件无法通过file://协议正常加载。这种情况建议起一个本地静态服务器或者至少在部署环境里测试渲染效果。顺手说一句macOS 上起本地服务很方便如果你还没配置过 JavaScript 开发环境在项目目录执行python3 -m http.server是最快的方案比装一大堆工具链省事很多。最后再分享一个小技巧公式编辑器做完了有一件事是我强烈建议补上的把“LaTeX 源码 渲染 HTML”同时保留下来导出时优先输出 LaTeX 源码而不是截图或innerHTML。因为后续不管是要把公式导入 Word、导出 PDF还是同步到其他文档系统LaTeX 源码都是最通用、最不容易出错的中间格式。我一开始没考虑这个等接到“把公式复制到 Word 还能编辑”的需求时回头补数据导出接口多花了不少时间。还有一个小细节工具栏按钮的style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />
上一篇/下一篇内容由系统自动关联
返回资讯列表 →