:KaTeX與MathJax選型及實現(xiàn))
簡介這是一份基于JavaScript與HTML5的網(wǎng)頁公式編輯器源碼包適合前端學習者、在線教育開發(fā)者或科研人員快速搭建數(shù)學公式輸入與繪圖功能。編輯器支持LaTeX/MathML公式解析、函數(shù)表達式輸入及圖形繪制并涉及事件監(jiān)聽、DOM交互、跨瀏覽器兼容與性能優(yōu)化等常見Web開發(fā)知識點整體代碼量精簡便于閱讀和二次改造。壓縮包內(nèi)共2個文件包含1個JavaScript邏輯文件與1個HTML結(jié)構(gòu)頁面包體僅9KB輕量易部署直接打開即可查看編輯與渲染效果。該資源發(fā)布以來已有1310人學習下載適合想通過實際案例掌握公式解析、Canvas/SVG繪圖及前端組件封裝的人群。通過閱讀代碼可以了解KaTeX/MathJax類庫的替代實現(xiàn)思路、函數(shù)圖像實時繪制流程以及在不依賴重型框架情況下完成公式編輯交互的可行方案對于正在做在線作業(yè)系統(tǒng)、數(shù)學工具站或需要公式輸入場景的開發(fā)者是一份實用的小型參考樣本。1. javascript公式編輯器在瀏覽器里寫公式這件事比你想的更繞給一個在線題庫或教務(wù)系統(tǒng)加公式錄入功能時產(chǎn)品經(jīng)理一句“就一個公式編輯器需求”背后其實拆成三層“公式怎么輸進去”“輸入之后怎么渲染出來”“存下來之后別人怎么再編輯”。標題里的 javascript公式編輯器并不是某個現(xiàn)成庫的名字而是用 JavaScript 在網(wǎng)頁里做“可錄入、可預(yù)覽、可回顯”的公式輸入能力。這類需求在習題批改、科研協(xié)作、低代碼表單里反復(fù)出現(xiàn)。難點不在“渲染一個公式”而在于讓不熟悉 LaTeX 的用戶也能順暢錄入讓熟悉 LaTeX 的用戶不被打斷同時保證最終落庫的是干凈、可逆的源數(shù)據(jù)。這三件事同時做到才叫一個能上線的公式編輯器。2. 公式編輯器的第一個岔路口渲染引擎與輸入形態(tài)怎么選2.1 MathJax 與 KaTeX兩個主流渲染引擎的取舍公式編輯器的地基是渲染引擎。當前主流是 MathJax 與 KaTeX 兩套。落地前先分清它們的性格比急著寫代碼重要。KaTeX 的核心優(yōu)勢是“快”。它是純前端渲染輸出的 HTML/CSS 比較輕頁面里一次性渲染幾百條公式也不吃力。KaTeX 由 TeX 排版系統(tǒng)衍生支持 LaTeX 語法的大部分常用子集像上下標、分式、根式、求和積分、矩陣、多行公式對齊環(huán)境都能處理。代價是“容錯差”遇到不合法的表達式直接拋錯不會像 MathJax 那樣“渲染出一個近似結(jié)果”。這個特點其實可以反過來用——把 KaTeX 渲染失敗當成用戶輸入的合法校驗器。另外 KaTeX 的字體和渲染樣式相對固定做精細排版定制比較費勁。MathJax 的優(yōu)勢是“全”和“穩(wěn)”。它在底層做了大量兼容遇到未識別命令、括號不配對、字體缺失等情況時會盡力給一個可讀的輸出不直接崩掉。MathJax 的啟動和渲染速度比 KaTeX 慢不少初次加載字體也多在頁面里動態(tài)插入公式時會有可感知的延遲。如果業(yè)務(wù)里公式以“整篇文檔排版”為主用戶對毫秒級反饋不敏感MathJax 更合適如果是表單里逐字符敲公式、需要預(yù)覽立刻跟上KaTeX 是更省心的選擇。還有一層要考慮MathJax 可以把一個公式渲染成 MathML這對對接無障礙閱讀器和結(jié)構(gòu)化文檔有幫助。KaTeX 也保留了解析后的源碼但生態(tài)里更多是“渲染 校驗”的玩法。我記得兩套引擎都還提供“自動掃描頁面中的美元符號/括號”的 auto-render 擴展但它更適合渲染已存在的內(nèi)容不適合做編輯器實時預(yù)覽。2.2 可視化輸入還是源碼輸入按目標用戶分岔渲染引擎只是輸出層真正的體驗分岔在“用戶怎么寫公式”。兩類方式最常見。第一類是源碼輸入用戶直接在文本框里寫 LaTeX 命令輸入的同時旁邊給實時預(yù)覽。這種方式對熟悉 LaTeX 的理工科用戶效率極高實現(xiàn)成本也最低。它對不熟悉 LaTeX 的人不友好——一個\frac{1}{2}可能勸退文科出身的運營同學。第二類是可視化輸入界面上擺著一排按鈕上下標、分式、根號、求和、希臘字母點按鈕往光標處插入命令模板用戶只填“空位”里的內(nèi)容。更高階的做法是像 MathQuill 那樣把公式渲染和光標定位合在一起用戶直接“所見即所得”地操作公式結(jié)構(gòu)完全不需要認識 LaTeX 命令。但這種方案的實現(xiàn)復(fù)雜度高出幾個量級涉及自定義光標管理、組合狀態(tài)、DOM 節(jié)點分類等做起來遠不止一個組件是一個完整的編輯內(nèi)核。我的判斷標準很簡單面向教師、科研人員、學生答題場景源碼輸入 實時預(yù)覽就夠配合工具欄按鈕能覆蓋 80% 的錄入效率需求面向行政表單、非專業(yè)錄入場景才值得上可視化編輯。大多數(shù)“javascript公式編輯器”的搜索訴求落在前者。先把這條主線做好再決定要不要加更多層。2.3 選型決策表五個維度定方向維度KaTeX 優(yōu)先的情況MathJax 優(yōu)先的情況實時預(yù)覽逐字符輸入預(yù)覽要零延遲整篇文檔渲染延遲感知弱出錯策略渲染失敗即報錯可復(fù)用為校驗盡力渲染不輕易打斷用戶質(zhì)量要求常用 LaTeX 子集夠用需要邊界語法、MathML、無障礙部署負載前端靜態(tài)文件即可字體多要考慮首次加載耗時定制空間樣式相對固定排版參數(shù)可調(diào)生態(tài)工具多如果實在搖擺我一般先按 KaTeX 走。原因是公式編輯器在絕大多數(shù)場景里的瓶頸是“輸入體驗”而輸入體驗對實時渲染速度最敏感。KaTeX 快、輕、能校驗夠用。等真遇到“必須渲染某種官方不支持命令”或“要對接 MathML 輸出”的業(yè)務(wù)再局部替換成 MathJax渲染接口并不復(fù)雜切換成本是可控的。3. 用 textarea 加 KaTeX 跑通最小公式編輯器可復(fù)制的完整代碼3.1 最小實現(xiàn)源碼與預(yù)覽雙欄不走工程腳手架先用一個單文件 HTML 把鏈路跑通。這就是一個“最小可用公式編輯器”左側(cè)寫 LaTeX 源碼右側(cè)實時渲染出錯時把錯誤信息顯示出來。代碼是完整的可以直接存成 html 打開看效果。!DOCTYPE html html langzh-CN head meta charsetUTF-8 title最小公式編輯器/title !-- 注意務(wù)必要同時引入 katex 的樣式與腳本缺樣式會渲染成純文本 -- link relstylesheet hrefhttps://cdn.jsdelivr.net/npm/katex0.16.11/dist/katex.min.css script srchttps://cdn.jsdelivr.net/npm/katex0.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 h3LaTeX 源碼 / 實時預(yù)覽/h3 div classtoolbar !-- 工具欄按鈕把命令模板插入光標處下面用 textarea 版本做示例 -- button>// 以 CodeMirror 6 為例示意編輯器實例與選區(qū)工具函數(shù) 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) { // 防抖渲染邏輯在這里觸發(fā) schedulePreview(update.state.doc.toString()); } }) ] }) }); // 取編輯器當前總內(nèi)容 function getLatex() { return view.state.doc.toString(); } // 用指定文本替換當前選區(qū) function replaceSelection(text) { view.dispatch({ changes: { from: view.state.selection.main.from, to: view.state.selection.main.to, insert: text } }); view.focus(); }邏輯說明updateListener是唯一的“內(nèi)容變化”入口所有對編輯器內(nèi)容的修改包括撤銷、粘貼、程序化替換都會觸發(fā)它比只監(jiān)聽鍵盤輸入可靠。docChanged判斷是為了避免光標移動等非內(nèi)容變化也觸發(fā)重渲染。replaceSelection里的from和to如果相等就等價于在光標處插入內(nèi)容。4.2 工具欄插入的完整邏輯光標、選區(qū)與轉(zhuǎn)義工具欄在多數(shù)字產(chǎn)品里都會保留。用 CodeMirror 后插入邏輯從 textarea 的selectionStart/End變成上面這種dispatch changes。有一個坑值得重點說LaTeX 里的反斜杠在 JavaScript 字符串里的寫法。// 錯誤寫法\frac{}{} 中的 \f 會被當成換頁符得到亂碼源串 const tmp1 \frac{}{}; // 正確寫法寫成雙反斜杠避免 JS 字符串轉(zhuǎn)義吃掉反斜杠 const tmp2 \\frac{}{}; // 模板字符串同樣要小心${} 會被當成插值 const tmp3 \\frac{${1}}{${\2\}}; // 注意內(nèi)層的雙反斜杠常見做法是把工具欄每個按鈕的命令模板維護在一個配置表里統(tǒng)一用雙反斜杠寫插入前再做一次斷言檢查if (template.indexOf(\\) -1) throw new Error(模板缺少反斜杠)。這層檢查雖然簡單但能攔下一類極其隱蔽的問題模板字符串里寫\frac后頁面渲染總是不對但源碼看起來又很正常控制臺也不報錯最后發(fā)現(xiàn)是字符串轉(zhuǎn)義把命令破壞了。工具欄插入時另一個加分項是“選中即包繞”。用戶用鼠標選中了1 2點“分式”按鈕期望得到的是\frac{1 2}{}而不是插入一個全新的空分式。要支持這個替換邏輯需要讀取選區(qū)文本function wrapWithCommand(command, argCount) { const main view.state.selection.main; const selected view.state.doc.sliceString(main.from, main.to) || ; let insertText command; // 把光標放到第一個空參數(shù)位實現(xiàn)從略示意思路 view.dispatch({ changes: { from: main.from, to: main.to, insert: insertText } }); }這個“包裹選區(qū)”的能力是把公式編輯器從“能用”做到“好用”的關(guān)鍵一步。按這個思路\sqrt{}、\frac{}{}、\int_{}^{}這類帶參數(shù)的模板都能和用戶操作對應(yīng)起來。4.3 可交付的編輯器還要有狀態(tài)提示、換行與模塊化工具欄和快捷鍵之外三個產(chǎn)品化細節(jié)別省。狀態(tài)提示編輯器下方保留狀態(tài)區(qū)展示“當前公式合法/包含 X 個未閉合大括號/渲染失敗”等信息。上面 3.1 里的errorMsg就是這個角色。這里不僅要顯示錯誤最好給出錯誤位置。KaTeX 的解析錯誤對象里有position屬性指向出錯字符在源碼中的偏移量可以用它把光標定位到出錯處。換行策略編輯器寬度有限源碼輸入必然需要換行。直接用\\和 LaTeX 的行寬語義綁定會干擾用戶。常見做法是編輯器內(nèi)允許肉眼換行硬換行但渲染前把行首行尾的空白trim掉再把行間換行合并成空格只把用戶明確輸入的\\當作 LaTeX 換行命令。這個策略我在前面 3.2 提過在 CodeMirror 版本里更好實現(xiàn)因為doc.toString()能精確拿到整篇源碼。模塊化把“編輯區(qū)、工具欄、預(yù)覽區(qū)、狀態(tài)區(qū)”拆成獨立組件對外暴露getLatex()、setLatex(text)、previewSourceChanged回調(diào)。入庫前業(yè)務(wù)層只需調(diào)getLatex()拿源碼串編輯時預(yù)覽可以由組件內(nèi)部完成半自動。這樣公式編輯器就是一個純前端組件與后端存儲結(jié)構(gòu)解耦。我曾見過把getLatex()寫在業(yè)務(wù)頁面里、每處都自己拼渲染參數(shù)的寫法換一個頁面就要改一遍后來統(tǒng)一收斂到組件內(nèi)才消停。5. 公式編輯器避坑記錄從渲染空白到粘貼亂碼5.1 渲染與顯示區(qū)三個高頻現(xiàn)象現(xiàn)象一公式區(qū)域一片空白控制臺沒有任何報錯。原因多數(shù)不是代碼邏輯而是 KaTeX 的 CSS 沒有加載或字體文件跨域被瀏覽器攔截。KaTeX 渲染輸出的結(jié)構(gòu)依賴自帶字體和樣式樣式缺失時渲染結(jié)果里有字符但被 CSS 隱藏或擠壓成不可見形態(tài)。解決檢查 Network 面板里katex.min.css及其引用的 woff2/ttf 是否都返回 200生產(chǎn)環(huán)境如果用了 CDN要確認字體文件路徑?jīng)]有被打包工具改壞本地部署時把katex.min.css與字體文件放在同一目錄用相對路徑引入?,F(xiàn)象二同一個公式在本地正常在客戶內(nèi)網(wǎng)環(huán)境渲染錯位。原因多為內(nèi)網(wǎng)瀏覽器版本過舊或字體渲染差異。KaTeX 對較老瀏覽器的支持有邊界部分單位內(nèi)網(wǎng)還是舊版 Chromium 內(nèi)核。解決先用兼容性表格確認團隊目標瀏覽器范圍如果內(nèi)網(wǎng)環(huán)境確實舊另一個方案是回到 MathJax 的 SVG 渲染output: svg把公式轉(zhuǎn)成 SVG 節(jié)點徹底繞開字體加載。代價是渲染速度下降但對內(nèi)網(wǎng)系統(tǒng)通??山邮堋,F(xiàn)象三輸入長公式時預(yù)覽越拖越卡CPU 占用飆升。原因常是每次input都全量渲染且公式里含大括號嵌套、多行環(huán)境時KaTeX 解析成本線性上升。解決防抖延遲拉高到 250~300ms在渲染前對比“當前源碼”與“上次渲染源碼”一致就跳過最激進的方式是把大公式拆成多個inline片段分別渲染或者切到 MathJax 的延遲渲染策略。多數(shù)場景防抖加比對就夠了。5.2 輸入與數(shù)據(jù)區(qū)兩個更深層的坑現(xiàn)象四前端預(yù)覽完全正常公式發(fā)給后端后亂了或解析失敗。原因是前端源碼在 JS 字符串處理過程中反斜杠被吞或者后端接手時對 LaTeX 源串做了未轉(zhuǎn)義處理。這是最隱蔽的一類問題。解決前端統(tǒng)一使用雙反斜杠模板并在getLatex()出口做一次校驗后端拿到字符串后打印原始字節(jié)看反斜杠數(shù)量是否和前端一致如果前后端之間走 JSON還要確認 JSON 序列化沒有二次轉(zhuǎn)義。庫表里存公式源串永遠不要只存渲染后的 HTMLHTML 不可逆且會膨脹?,F(xiàn)象五用戶從 Word 里復(fù)制公式Office MathML 或 MathType 生成的富文本粘貼進編輯器得到一坨亂碼。原因是粘貼時瀏覽器默認把剪貼板中的text/html交給光標處包含大量 XML 標簽和私有標記textarea 或 CodeMirror 會把這些當純文本塞進去。解決攔截編輯器區(qū)域的paste事件用clipboardData.getData(text/plain)只取純文本寫入如果業(yè)務(wù)上確實需要支持 Word 公式轉(zhuǎn)換取到純文本后先做一輪清洗把!--、等 XML 痕跡剔除或者走一個獨立的“Word 公式轉(zhuǎn) LaTeX”轉(zhuǎn)換工具。最保守的底線是粘貼進來的任何內(nèi)容必須先校驗是合法 LaTeX再允許入庫。6. 進階離屏渲染、圖片導(dǎo)出與公式三段校驗法6.1 離屏渲染與導(dǎo)出圖片集成到文檔導(dǎo)出鏈路公式編輯器在在線文檔、報告生成、題庫導(dǎo)出場景里的最終產(chǎn)物不只是“頁面里能看”還要能落到 PDF 或 Word 里。常見做法是離屏渲染新建一個不在視口中的 DOM 容器把同一個 LaTeX 源碼渲染進去然后借助瀏覽器能力導(dǎo)出圖片或 SVG。// 離屏渲染把公式轉(zhuǎn)成 SVG 字符串供后續(xù)導(dǎo)出或提交后端 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 配合結(jié)構(gòu)化需求 throwOnError: true }); return temp.querySelector(svg) ? temp.innerHTML : ; } finally { document.body.removeChild(temp); } }這段邏輯說明把容器移到屏幕外避免用戶看到渲染瞬間的閃爍throwOnError: true用于保證不合格公式能拋錯被上層捕獲。圖片導(dǎo)出時可以拿這個 SVG 字符串去繪制img也可以交給服務(wù)端轉(zhuǎn)成 PNG。離屏渲染不占用主預(yù)覽區(qū)導(dǎo)出和編輯是兩個互不干擾的線程過程。6.2 公式三段校驗法渲染只是第一層公式經(jīng)過編輯器后數(shù)據(jù)質(zhì)量決定后續(xù)能不能復(fù)用。我習慣在項目里固定一條三段校驗流程第一步渲染校驗前端用 KaTeX 渲染源碼拋錯即攔截第二步結(jié)構(gòu)校驗用解析器檢查括號配對、命令結(jié)尾、未知環(huán)境這幾項攔截一些“能渲染出來但語義不對”的輸入第三步入庫后回讀校驗從數(shù)據(jù)庫讀出源串再渲染一次確認存取過程沒有轉(zhuǎn)義損耗。校驗層位置手段目標渲染校驗前端預(yù)覽KaTeX / MathJax 渲染拋錯攔截寫不了的表達式結(jié)構(gòu)校驗前端/服務(wù)端括號配對、命令白名單攔截能渲染但語義錯的輸入回讀校驗后端重讀庫表再渲染確認序列化無損耗這個三層校驗做完公式數(shù)據(jù)的可信度才穩(wěn)。圖片導(dǎo)出、回聲測試、無障礙輸出這些能力都可以間接復(fù)用這套鏈路。運行環(huán)境和集成方式會變——從單頁表單到富文本編輯器再到大文檔系統(tǒng)——但“輸入源碼、實時渲染、嚴格校驗、按需導(dǎo)出”這條主線不變。我做一個公式編輯器時最后都會保留這四件套后面接什么場景都順。前幾次做這類需求時我只顧著渲染好不好看后來被線上數(shù)據(jù)亂碼和導(dǎo)出錯版教了一課才把校驗工序提到這個位置。這套流程運行一段時間后你可以明顯感到維護成本降下來了——畢竟公式出錯的來源大多就那么幾個。希望幫到你。本文還有配套的精品資源點擊獲取