
简介这是一份内置数学公式能力的 UEditor 富文本编辑器资源包主要面向需要在网页中实现图文与公式混合编辑的 Web 开发者可将百度 UEditor 与 kityformula-plugin 公式插件直接结合使用解决普通编辑器无法输入 LaTeX 数学公式的问题。压缩包共 747 个文件约 3.43MB以 PNG 图片、JS 脚本、CSS 样式、HTML 示例页面为主同时包含 GIF、字体、Flash 与少量可执行文件便于完整还原插件所需的前端依赖。已有 1463 人学习浏览适合需要快速在项目中集成公式编辑功能的初中级前端开发者。资源提供了完整的编辑器目录和可运行页面能直接抽取 UEditor 及公式插件相关文件进行二次开发。整体体积轻量、结构清晰可用于教学演示、题库系统或论文写作平台等场景节省自行整合与调试的时间。1. 给UEditor加公式插件比想象中坑多但值得做给UEditor加一个公式插件很多人第一反应是先让用户传图片吧。图片公式看着省事可用户要改一个符号就得重新截图题库想检索题目公式也完全没辙。这个标题做的事很具体在UEditor编辑器里集成一个带公式插件的版本让用户能输入LaTeX源码、实时看到渲染效果、保存后还能二次编辑。适合正在做题库、在线作业、课程平台或技术文档系统的人。只要你的用户需要在网页里写数学公式这套方案就能把图片方案积累出来的维护成本一次性压下来。听起来不复杂但真正落地时公式插件会撞上UEditor的内容过滤、MathJax的加载时机、LaTeX的转义这几个老问题。下文就按选型→集成→数据流转→踩坑→打磨这条线把一条能直接用起来的完整路径讲清楚。2. 公式插件选型为什么LaTeX源码加前端渲染是主流方案2.1 先想清楚公式在系统里的三种形态任何公式插件说到底是回答一个问题用户输入的公式在你的系统里以什么形态存在。我见过三种形态对应三种完全不同的维护成本。第一种是图片形态。用户在面板里输完公式系统渲染成PNG或SVG图片编辑器里插一张img后台存图片地址。这种方案开发最快显示效果也最稳定代价是公式一旦保存就死了。改一个符号要重新生成图片想对公式做全文检索得先接OCR图片存储量和CDN流量也会随时间涨上去。临时展示可以题库、作业这类需要反复编辑的场景后期运维成本会持续膨胀。第二种是渲染后的静态HTML/SVG形态。公式被转成SVG或MathML直接写进DOM编辑器里看到的就是最终视觉稿不依赖任何运行时渲染库。显示非常稳定但它同样面临能不能还原成源码的问题。保存时只存渲染结果想改公式就得删掉重来同时存源码和渲染结果又会出现双份数据同步困难。只做单次输入、后续不再编辑的纯展示系统可以考虑它。第三种是LaTeX源码加渲染层。编辑器内部存的是\frac{a}{b}这样的源码显示时由MathJax这类前端渲染库在浏览器里实时生成视觉稿。源码是纯文本能存数据库、能参与检索、能被编辑器二次加载这是它最大的优势。缺点也明显渲染库的加载时机、公式转义、复制粘贴丢失源码这些坑要在实现层一个个填平。我的判断是只要你的系统存在用户改公式系统检索公式公式跨端显示任意一条需求就值得走第三种形态。这也是标题里公式插件最常见、最可靠的落地选择。2.2 三个可落地方案对比把输入和显示拆开看可落地的组合大致是这三类我列过一张对比表方案输入方式显示/保存优点主要成本A 图片公式面板截图或系统渲染图片保存img地址开发量最小、显示零误差不可编辑、不可检索存储和CDN成本高B 静态HTML面板实时预览后生成SVG/HTML保存渲染结果显示稳定、不依赖运行时渲染二次编辑需另存源码数据双份C LaTeX源码前端渲染输入框符号按钮所见即所得保存源码显示时渲染可编辑、可检索、可二次开发转义、渲染时机、复制粘贴坑多做选型时有一个容易被忽略的点输入面板做成什么样决定这套系统能不能被普通编辑接受。纯手打LaTeX门槛不低一般人记不住矩阵的\begin{matrix}、记不住\left( \right)。我一般会把面板设计成符号按钮输入框实时预览三件套常用符号做成按钮点一下往输入框插对应源码输入框下方用MathJax实时渲染预览。这样录入者只需要知道分数对应\frac{a}{b}不需要背完整语法。2.3 数据流转设计占位符是唯一真源选定C方案后最重要的不是界面而是数据流转协议。协议没定好后面接后端、做回显、做检索时一定会返工。我一般会这样做约定。编辑器内部一条公式始终是一个完整的占位符节点。这个节点长这样span classformula contenteditablefalse>// 注册公式工具栏按钮 UE.registerUI(formula, function (editor) { var dialog new UE.ui.Dialog({ iframeUrl: editor.options.UEDITOR_HOME_URL dialogs/formula/formula.html, editor: editor, name: formula, title: 插入公式, cssRules: width:680px;height:440px, onok: function () { // 点击对话框【确定】后从面板页面里取LaTeX源码 var win this.iframe.contentWindow; var latex win.getFormulaLatex(); // 面板暴露的取值函数 if (latex) { editor.execCommand(insertFormula, latex); } } }); var btn new UE.ui.Button({ name: formula, title: 公式, onclick: function () { dialog.open(); } }); return btn; });逻辑说明UE.registerUI的第一个参数是按钮名第二个参数是工厂函数返回一个按钮实例。onok是对话框点确定的回调此时从对话框iframe里取用户输入完成的LaTeX源码然后交给自定义命令insertFormula去执行插入。iframeUrl拼上UEDITOR_HOME_URL保证项目部署在子路径时也能定位到面板页面。cssRules控制对话框的宽高这里给的是常规尺寸移动端适配会在避坑章节单独说。第二步注册插入命令editor.registerCommand(insertFormula, { execCommand: function (cmd, latex) { buildFormulaSpan(latex).then(function (html) { if (html) { editor.execCommand(insertHtml, html); } }); } });把插入公式封装成命令而不是直接在按钮里写逻辑好处是后续做二次编辑、做批量导入导出时可以复用同一段生成逻辑不会出现两条路写两遍。命令里调用buildFormulaSpan生成占位符HTML这个函数在3.4给出完整实现。3.3 公式输入面板符号按钮、输入框与实时预览对话框页面formula.html的骨架很简单核心是三个区域!-- dialogs/formula/formula.html -- textarea idlatexInput rows4 placeholder输入 LaTeX如 \frac{a}{b}/textarea div idpreview\( \)/div div idsymbols/div面板逻辑里要做三件事渲染符号按钮、向输入框插入符号、触发预览刷新。// formula.js var input document.getElementById(latexInput); var preview document.getElementById(preview); // 符号按钮显示名 与 插入的LaTeX片段 var SYMBOLS [ [分数, \\frac{a}{b}], [根号, \\sqrt{a}], [上标, a^{b}], [下标, a_{b}], [α, \\alpha], [求和, \\sum_{i1}^{n}], [积分, \\int_{a}^{b}], [矩阵, \\begin{matrix}a b\\\\c d\\end{matrix}] ]; function insertAtCursor(textarea, text) { var start textarea.selectionStart; var end textarea.selectionEnd; textarea.value textarea.value.slice(0, start) text textarea.value.slice(end); textarea.selectionStart textarea.selectionStart text.length; textarea.selectionEnd textarea.selectionStart; textarea.focus(); } function refreshPreview() { preview.innerHTML \\( input.value \\); // MathJax 已在 formula.html 中加载 if (window.MathJax) { MathJax.typesetPromise([preview]).catch(function (err) { console.log(latex render error:, err); }); } } SYMBOLS.forEach(function (item) { var btn document.createElement(button); btn.textContent item[0]; btn.type button; btn.onclick function () { insertAtCursor(input, item[1]); refreshPreview(); }; document.getElementById(symbols).appendChild(btn); }); input.addEventListener(input, refreshPreview); // 暴露给父窗口的取值函数 window.getFormulaLatex function () { return input.value.trim(); };逻辑说明符号按钮的onclick做的事情是在光标处插入模板文本所以用户输入的既有内容不会丢。refreshPreview把输入框内容包进\(...\)渲染到预览区。最后的window.getFormulaLatex是对话框父窗口调用的接口父窗口的onok回调正是通过它拿到LaTeX源码。符号表按项目需要扩充数学公式多的系统可以加入矩阵、分段函数\begin{cases}、极限\lim等模板。这里有一个小技巧模板里的a、b用占位符用户点分数后直接全选把a换成实际内容比空模板更友好。3.4 插入占位符渲染HTML与源码绑定占位符的生成逻辑是整个插件最关键的一段它决定公式在编辑器里长什么样、保存时带不带源码// 主页面生成带渲染结果的公式span function buildFormulaSpan(latex) { var wrap document.createElement(div); wrap.style.position absolute; wrap.style.left -9999px; wrap.innerHTML \\( latex \\); document.body.appendChild(wrap); // MathJax渲染是异步的需要等它完成再拿HTML return MathJax.typesetPromise([wrap]).then(function () { var rendered wrap.innerHTML; wrap.parentNode.removeChild(wrap); var span document.createElement(span); span.className formula; span.setAttribute(contenteditable, false); span.setAttribute(data-latex, encodeFormulaAttr(latex)); span.setAttribute(style, display:inline-block;max-width:100%;overflow-x:auto;vertical-align:middle;); span.innerHTML rendered; return span.outerHTML; }); }逻辑说明先在主页面一个隐藏容器里让MathJax渲染\(...\)渲染完成后再把容器内的HTML取出来包成带>var ue UE.getEditor(editor, { whiteList: { span: [class, data-latex, contenteditable, style] } });注意UEditor的whiteList是全局配置直接覆盖默认值时要把span原本允许的属性如id、title一并带上否则默认能力会被误伤。我在实际项目中更倾向于用addOutputRule保护公式节点因为改全局白名单影响面太大容易让其他安全过滤失效。第二处是后端入库时只存了纯文本把HTML标签全部剥离。公式源码若是放在>ue.ready(function () { // 输出规则里保护公式遇到classformula的span保留data-latex ue.addOutputRule(function (root) { var nodes root.getNodesByTagName(span); for (var i 0, len nodes.length; i len; i) { if (nodes[i].getAttribute(class) formula) { nodes[i].setAttribute(_keep, 1); } } }); });这段代码的作用是标记公式节点让后续的过滤逻辑对它放行。addOutputRule的回调会接收到解析后的DOM树可以按标签遍历并修改节点比正则处理HTML字符串靠谱得多。4.2 编码与转义LaTeX源码的存储协议LaTeX源码里高频出现反斜杠\、花括号{}、尖括号 这些字符放进HTML属性会被浏览器自动转义。比如会变成lt;反斜杠在某些后端框架里还会被当成转义字符处理。最省心的做法是属性里存encodeURIComponent之后的文本function encodeFormulaAttr(latex) { return encodeURIComponent(latex); } function decodeFormulaAttr(encoded) { try { return decodeURIComponent(encoded); } catch (e) { return encoded; // 解码失败时原样返回至少不丢数据 } }后端存储时公式源码要作为一个独立字段原样存取不要走富文本HTML的实体反转义流程。很多系统的富文本字段在存入时会做strip_tags或htmlspecialchars公式源码一旦混进这个流程\和{}基本会被改写。协议上明确公式源码字段是纯文本、不做HTML转义服务端和编辑器端各守一端问题就能避免。历史数据清洗也值得注意。早期版本如果保存的是\(...\)纯文本标记需要写一个转换函数把这类标记替换成规范的span占位符再灌回编辑器。转换要放到setContent之前做否则编辑器会把\(当普通文本放进DOM。4.3 回显链路从后端HTML还原成可编辑状态详情页回显和再次编辑回显是两条路径处理方式不同。详情页回显最简单后端把包含公式span的HTML直接输出到页面页面加载MathJax后执行一次typesetPromise公式就会重新渲染。占位符里的HTML本身也是渲染结果所以即使typeset失败页面上也不是空白。这里有个小技巧给公式容器加个overflow-x:auto长公式不会撑破卡片。再次编辑的回显要复杂一点要把数据还原成编辑器可操作的DOMue.ready(function () { // savedHtml 是从后端拿到的、包含公式span的完整HTML if (savedHtml) { // 如果历史数据里公式是纯文本标记先转换再回填 savedHtml normalizeFormulaMarkers(savedHtml); ue.setContent(savedHtml); } });normalizeFormulaMarkers做的事是把\(...\)、\[...\]这样的纯文本标记替换成带>ue.ready(function () { var doc ue.document; UE.dom.domUtils.on(doc, click, function (e) { var target e.target; var formula findParentFormula(target); // 向上找classformula的节点 if (!formula) return; e.preventDefault(); var latex decodeFormulaAttr(formula.getAttribute(data-latex)); // 打开对话框并预填 var dlg openFormulaDialogWith(latex); // 复用3.2的dialog传入初始值 dlg.onok function () { var newLatex dlg.iframe.contentWindow.getFormulaLatex(); if (!newLatex || newLatex latex) return; buildFormulaSpan(newLatex).then(function (html) { var temp document.createElement(div); temp.innerHTML html; var newFormula temp.firstChild; formula.parentNode.replaceChild(newFormula, formula); }); }; }); }); function findParentFormula(node) { var depth 0; while (node depth 4) { if (node.getAttribute node.getAttribute(data-latex)) return node; if (node.classList node.classList.contains(formula)) return node; node node.parentNode; depth; } return null; }逻辑说明findParentFormula向上最多找4层覆盖点到公式内部SVG节点的情况。openFormulaDialogWith给对话框加一个初始化值的入口打开时把源码塞进输入框并刷新预览。替换节点用replaceChild而不是重新插入保证公式在文档中的位置不变前后文不粘连。这里有一个容易翻车的细节点击事件要绑定在ue.document上而不是主页面document上因为UEditor是iframe模式两者的DOM完全隔离。二次编辑做完整后整个闭环才算成立输入→保存→回显→再编辑→再保存。这也是判断一个公式插件能不能用的分水岭——只做插入不做编辑的方案用起来会非常憋屈。5. 公式插件避坑指南5个高频翻车点公式插件的坑不在功能开发而在数据流转的边界上。下面5个问题按现象→原因→解决的顺序说清楚每一类都值得在开发前就做好预案。5.1 公式渲染成裸LaTeX源码现象插入后编辑器里显示的是\frac{a}{b}文本不是公式视觉稿或者第一次显示正常复制粘贴后变成源码。原因MathJax没有覆盖到UEditor的编辑区。UEditor默认用iframe作为编辑区MathJax如果只挂载在主页面document上iframe里的新节点不会被自动渲染。另外buildFormulaSpan的主页面渲染方案要求等待Promise完成后再插入若等不及先插了空span用户看到的也会是源码文本。解决统一走主页面隐藏容器渲染→取回HTML→插入占位符的路径不要试图在UEditor的iframe里实时typeset。渲染完成后插入前检查返回的HTML是否为空为空时给公式加一个渲染失败的兜底样式避免插入空节点。兜底样式可以是灰底源码文本至少用户知道这里有一个公式出了问题。5.2 保存后再打开公式被过滤掉现象第一次保存正常重新打开编辑页公式要么变成普通文本要么style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />