☰
UEditor公式插件集成实战:LaTeX渲染、占位符设计与避坑指南
2026/10/14 23:44:20 网站建设 项目流程

简介:这是一份内置数学公式能力的 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 class="formula" contenteditable="false">// 注册"公式"工具栏按钮 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 id="latexInput" rows="4" placeholder="输入 LaTeX,如 \frac{a}{b}"></textarea> <div id="preview">\( \)</div> <div id="symbols"></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_{i=1}^{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 () { // 输出规则里保护公式:遇到class=formula的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,长公式不会撑破卡片。

再次编辑的回显要复杂一点,要把数据还原成编辑器可操作的DOM:

ue.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); // 向上找class=formula的节点 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;" />

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询