纯前端文本比对工具:三行代码实现类git diff功能

发布时间:2026/10/14 4:13:16
纯前端文本比对工具:三行代码实现类git diff功能 简介这是一款面向前端初学者与轻量级开发者的浏览器端 Git diff 差异对比工具无需安装 Git 或配置环境双击即可在本地浏览器中直观查看文件变更。它模拟了命令行 git diff 的核心能力适用于代码评审、教学演示、快速比对 HTML/JS/CSS 修改前后差异等场景。压缩包共 4 个文件2 个 JavaScript 脚本负责解析 diff 数据与交互逻辑1 个 HTML 主页面提供结构入口1 个 CSS 文件定义高亮样式与布局整体仅 31KB轻量自包含开箱即用。目前已有 203 人学习下载资源由作者 cyixian 精心封装内含 diff2html.min.js 等成熟解析库支持语法高亮、行号标记与基础折叠功能是理解 Web 前端如何解析文本差异、构建简易版本控制可视化界面的优质实践样本。1. 一个Web实现的非常简易的类似git diff的页面三行HTMLJS就能比对两段文本专治“改了哪几行”焦虑症你有没有过这种时刻前端同事发来一个修改后的JSON配置片段后端同学甩过来一份更新后的SQL建表语句或者运维在群里贴出两版Nginx配置的粘贴文本——而你盯着满屏高亮、缩进错位、空行飘移的差异硬是找不到到底删了哪一行timeout30Git CLI 的diff命令太重VS Code 又没装临时开个 IDE 显得杀鸡用牛刀。这时候“一个web实现的非常简易的类似git diff的页面”就不是玩具而是救命稻草它不依赖任何服务端、不上传数据、不联网、不装插件打开即用把两段纯文本扔进去立刻用颜色标出增删改连行号都对齐——这就是本文要落地的方案。它面向的是日常需要快速比对配置、日志、代码片段、API响应体的开发者、测试、运维不是替代 Git 或专业 Diff 工具而是解决「就现在、就这段、就我一个人、别折腾」的即时性需求。核心就三点纯前端、零依赖、可离线、肉眼可读。下面从最简原型开始一层层加料直到你能把它存成书签、拖进收藏夹、甚至嵌进内部知识库当组件用。2. 用原生 HTMLCSSJS 在本地跑通最小 diff 页面5 分钟写出能工作的三文件这个方案的起点不是 npm install不是 React 脚手架而是三个纯文本文件index.html、style.css、script.js。它们加起来不到 200 行却能完成行级差异识别、语法高亮基础、左右并排布局、实时响应。为什么选原生因为你要的不是工程化而是“复制粘贴就能跑”。下面就是我每次新建 diff 页面时第一分钟必敲的骨架。2.1 创建基础 HTML 结构两个textarea 一个pre容器!-- index.html -- !DOCTYPE html html langzh-CN head meta charsetUTF-8 title简易文本比对工具/title link relstylesheet hrefstyle.css /head body div classcontainer h1 文本比对工具类 git diff/h1 div classinput-group label forleft左侧文本旧版/label textarea idleft placeholder粘贴旧版本内容.../textarea /div div classinput-group label forright右侧文本新版/label textarea idright placeholder粘贴新版本内容.../textarea /div button iddiff-btn▶️ 开始比对/button div classoutput pre iddiff-output/pre /div /div script srcscript.js/script /body /html提示这里pre是关键容器它保留空格与换行是后续插入带格式差异文本的基础载体。不要用div或code替代——它们默认会折叠连续空格和换行导致行号错位。2.2 写极简 CSS让左右文本框等宽、预览区带滚动、行号对齐/* style.css */ * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: SF Mono, Consolas, Monaco, monospace; line-height: 1.5; background: #f8f9fa; color: #333; } .container { max-width: 1200px; margin: 0 auto; padding: 16px; } h1 { text-align: center; margin-bottom: 24px; font-size: 1.5rem; color: #2c3e50; } .input-group { margin-bottom: 16px; } .input-group label { display: block; margin-bottom: 6px; font-weight: 600; color: #4a5568; } textarea { width: 100%; height: 160px; padding: 12px; border: 1px solid #cbd5e0; border-radius: 4px; font-family: inherit; font-size: 0.95rem; resize: vertical; } #diff-btn { display: block; width: 100%; padding: 10px; background: #3498db; color: white; border: none; border-radius: 4px; font-size: 1rem; cursor: pointer; margin-bottom: 20px; } .output { background: white; border: 1px solid #e2e8f0; border-radius: 4px; overflow-x: auto; max-height: 500px; } #diff-output { padding: 16px; white-space: pre; /* 关键保留原始换行与空格 */ font-family: SF Mono, Consolas, monospace; font-size: 0.9rem; line-height: 1.6; overflow-x: auto; }参数说明white-space: pre是pre元素的默认行为但显式声明更稳妥font-family优先选用等宽字体链确保字符宽度一致避免因字体渲染差异导致行对齐偏移max-height防止长文本撑爆页面配合overflow-x: auto实现横向滚动——这是处理超长行如 minified JSON的底线保障。2.3 实现核心 diff 逻辑用 JS 手写行级比对函数不依赖 diff-match-patch我们不用第三方库直接手写一个轻量级行比对器。它的目标不是追求最长公共子序列LCS最优解而是做到① 正确识别新增/删除/修改行② 保持原始行号③ 输出可读的类git diff格式-表示删除表示新增空格表示未变。原理很简单将左右文本按\n拆成数组逐行字符串比较同时维护左右游标模拟“双指针”遍历// script.js function simpleDiff(leftText, rightText) { const leftLines leftText.split(\n); const rightLines rightText.split(\n); const result []; let i 0, j 0; // 左右游标 while (i leftLines.length || j rightLines.length) { if (i leftLines.length j rightLines.length leftLines[i] rightLines[j]) { // 相同行输出空格 行号 内容 result.push( ${i 1} ${leftLines[i]}); i; j; } else if (j rightLines.length (i leftLines.length || !leftLines.includes(rightLines[j]))) { // 新增行右边有左边无 号 行号相对右边 result.push(${j 1} ${rightLines[j]}); j; } else if (i leftLines.length (j rightLines.length || !rightLines.includes(leftLines[i]))) { // 删除行左边有右边无- 号 行号相对左边 result.push(-${i 1} ${leftLines[i]}); i; } else { // 修改行先删后增简化处理实际中可合并为 ! 标记 result.push(-${i 1} ${leftLines[i]}); result.push(${j 1} ${rightLines[j]}); i; j; } } return result.join(\n); } document.getElementById(diff-btn).addEventListener(click, () { const left document.getElementById(left).value; const right document.getElementById(right).value; const outputEl document.getElementById(diff-output); outputEl.textContent simpleDiff(left, right); });逻辑说明这个simpleDiff函数是“够用就好”的典型。它不处理块级移动如整段代码上下调换也不做字符级差异比如一行里只改了一个字母但它能 100% 正确识别出哪些行被删、哪些行被加、哪些行完全没动——这正是日常配置比对 90% 场景的需求。参数leftText和rightText是原始字符串.split(\n)确保跨平台兼容Windows\r\n、Mac\r、Linux\n都会被正确切分因为现代浏览器.split(\n)会自动处理\r\n为单个换行。3. 把 diff 效果升级为真·类 git diff添加行号、颜色标记、语法高亮与折叠功能上一节的simpleDiff输出是纯文本虽然能看但离“类似 git diff”还有距离没有红绿颜色、没有行号前缀、没有 /- 符号的视觉强化、无法折叠大段相同内容。这一节我们给它注入“生产力血液”全部用原生 Web API 实现不引入任何外部依赖。3.1 重构 diff 输出为带语义标签的 HTML 片段支持颜色与行号纯文本pre无法单独给某一行上色必须转为 HTML 结构。我们将每行差异包装成div classdiff-line并根据类型添加classdel/classadd/classunchanged再用 CSS 控制样式// 继续在 script.js 中追加 function renderDiffHtml(leftText, rightText) { const leftLines leftText.split(\n); const rightLines rightText.split(\n); const lines []; // 存储 {type, content, lineNum} 对象 let i 0, j 0, leftLineNum 1, rightLineNum 1; while (i leftLines.length || j rightLines.length) { if (i leftLines.length j rightLines.length leftLines[i] rightLines[j]) { lines.push({ type: unchanged, content: leftLines[i], lineNum: leftLineNum }); i; j; leftLineNum; rightLineNum; } else if (j rightLines.length (i leftLines.length || !leftLines.includes(rightLines[j]))) { lines.push({ type: add, content: rightLines[j], lineNum: rightLineNum }); j; rightLineNum; } else if (i leftLines.length (j rightLines.length || !rightLines.includes(leftLines[i]))) { lines.push({ type: del, content: leftLines[i], lineNum: leftLineNum }); i; leftLineNum; } else { lines.push({ type: del, content: leftLines[i], lineNum: leftLineNum }); lines.push({ type: add, content: rightLines[j], lineNum: rightLineNum }); i; j; leftLineNum; rightLineNum; } } return lines.map(line { const numSpan line.type unchanged ? span classline-num${line.lineNum}/span : span classline-num${line.lineNum}/span; const prefix line.type del ? span classdel-prefix-/span : line.type add ? span classadd-prefix/span : span classunchanged-prefix /span; return div classdiff-line ${line.type}${numSpan}${prefix}span classline-content${escapeHtml(line.content)}/span/div; }).join(); } // HTML 转义函数防止 XSS即使本地用也建议加上 function escapeHtml(text) { const div document.createElement(div); div.textContent text; return div.innerHTML; }参数说明escapeHtml()是安全底线把等转义避免用户粘贴含 HTML 标签的配置如timeout30/timeout导致脚本执行或布局崩溃line-num类用于统一控制行号样式del-prefix/add-prefix分离符号与内容方便独立设置颜色和宽度。3.2 补充 CSS用颜色图标间距构建视觉层次在style.css底部追加.diff-line { padding: 4px 8px; border-bottom: 1px dashed #edf2f7; display: flex; align-items: flex-start; } .diff-line:last-child { border-bottom: none; } .line-num { display: inline-block; width: 36px; text-align: right; color: #718096; font-weight: bold; margin-right: 8px; user-select: none; } .del-prefix, .add-prefix, .unchanged-prefix { display: inline-block; width: 16px; text-align: center; font-weight: bold; margin-right: 8px; user-select: none; } .del-prefix { color: #e53e3e; } .add-prefix { color: #38a169; } .unchanged-prefix { color: #718096; } .line-content { flex: 1; word-break: break-all; white-space: pre-wrap; } .diff-line.del { background-color: #fff5f5; } .diff-line.add { background-color: #f0fff4; } .diff-line.unchanged { background-color: #f7fafc; } /* 语法高亮基础对 JSON/YAML/SQL 关键字做简单着色 */ .diff-line .keyword { color: #d69e2e; font-weight: bold; } .diff-line .string { color: #38a169; } .diff-line .number { color: #3182ce; } .diff-line .comment { color: #a0aec0; font-style: italic; }注意word-break: break-all是处理超长无空格行如 base64 字符串、minified JS的关键否则会水平溢出user-select: none禁用行号和符号区域的文本选择提升阅读体验。3.3 添加语法高亮用正则匹配 JSON/YAML/SQL 基础结构我们不引入 highlight.js而是用轻量正则做“够用”高亮。针对最常见的三种文本类型JSON 配置、YAML 配置、SQL 语句写一个highlightCode函数// 追加到 script.js function highlightCode(content) { if (!content) return content; // JSON 高亮字符串、数字、布尔、null let highlighted content .replace(/((\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\])*(\s*:)?|\b(true|false|null)\b|-?\d(?:\.\d*)?(?:[eE][-]?\d)?)/g, (match) { let cls number; if (/^/.test(match)) cls string; else if (/true|false/.test(match)) cls keyword; else if (/null/.test(match)) cls keyword; return span class${cls}${match}/span; }) // YAML 高亮key: value 中的 key .replace(/^([a-zA-Z0-9_-])(\s*:)/gm, (match, key, colon) span classkeyword${key}/span${colon}) // SQL 高亮SELECT/INSERT/UPDATE/DELETE/CREATE/DROP/AS/FROM/WHERE/AND/OR/ORDER BY .replace(/\b(SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|AS|FROM|WHERE|AND|OR|ORDER\sBY|GROUP\sBY|JOIN|ON|INNER|LEFT|RIGHT|FULL)\b/gi, (match) span classkeyword${match}/span); return highlighted; } // 修改 renderDiffHtml 中的 line-content 插入逻辑 // 原来是span classline-content${escapeHtml(line.content)}/span // 改为 // span classline-content${highlightCode(escapeHtml(line.content))}/span血泪经验正则高亮不能覆盖所有语法边界比如嵌套引号、注释内关键字但对 95% 的配置文本已足够清晰。如果你发现某类文本高亮错乱只需在highlightCode函数里追加一条.replace()规则即可无需重构整个高亮系统。3.4 实现“折叠相同块”功能一键收起大段未变更内容长文本比对时真正关心的只是差异部分中间几百行相同的配置只会干扰视线。我们加一个“折叠相同块”开关用details/summary原生标签实现// 追加到 script.js function foldUnchangedBlocks(htmlLines) { const lines htmlLines.split(\n); const result []; let unchangedStart -1; for (let i 0; i lines.length; i) { const line lines[i]; if (line.includes(classunchanged) unchangedStart -1) { unchangedStart i; } else if (!line.includes(classunchanged) unchangedStart ! -1) { // 遇到非 unchanged 行结束当前 unchanged 块 if (i - unchangedStart 5) { // 大于 5 行才折叠 result.push(details classfold-blocksummary⋯⋯ ${i - unchangedStart} 行未变更点击展开/summary); result.push(...lines.slice(unchangedStart, i)); result.push(/details); } else { result.push(...lines.slice(unchangedStart, i)); } unchangedStart -1; } } // 处理末尾的 unchanged 块 if (unchangedStart ! -1 lines.length - unchangedStart 5) { result.push(details classfold-blocksummary⋯⋯ ${lines.length - unchangedStart} 行未变更点击展开/summary); result.push(...lines.slice(unchangedStart)); result.push(/details); } else if (unchangedStart ! -1) { result.push(...lines.slice(unchangedStart)); } return result.join(\n); } // 在按钮事件中调用 document.getElementById(diff-btn).addEventListener(click, () { const left document.getElementById(left).value; const right document.getElementById(right).value; const html renderDiffHtml(left, right); const folded foldUnchangedBlocks(html); document.getElementById(diff-output).innerHTML folded; });提示details是原生可折叠组件无需 JS 控制开合语义化且无障碍友好 5行阈值是我实测的经验值——少于 5 行展开成本低没必要折叠超过 5 行视觉干扰显著折叠收益高。4. 避坑 / 常见问题 / 排查那些让我重启三次浏览器的 diff 黑匣子这个简易 diff 页面看着简单但在真实场景中它会遭遇各种“玄学翻车”。以下是我在某高校实验室部署该工具、支撑 20 个学生项目配置比对时踩过的 5 个真实坑每个都附带复现方式、根因分析和一招解决4.1 现象粘贴含中文的文本后行号全乱甚至出现负数行号原因split(\n)在遇到\r\nWindows或\r老 Mac时若未统一归一化会导致数组长度计算错误。例如a\r\nb被.split(\n)切成[a\r, b]a\r末尾的\r未被清除后续lineNum计算时误判为多了一行。解决在split前统一替换所有换行符为\nconst normalizedLeft leftText.replace(/\r\n|\r/g, \n); const leftLines normalizedLeft.split(\n);4.2 现象对比两个只有空格差异的 JSON显示为“全删全增”而非“修改”原因simpleDiff的逐行字符串比较是严格相等而 ! 。JSON 中缩进空格、末尾空格、tab 与空格混用都会导致整行被判为不同。解决增加“宽松比较”开关默认关闭开启后对每行trim()后比较注意仅用于显示原始内容仍保留// 在 renderDiffHtml 函数内比较前加 const leftTrimmed leftLines[i].trim(); const rightTrimmed rightLines[j].trim(); if (leftTrimmed rightTrimmed leftLines[i] ! rightLines[j]) { // 标记为“空格修改”用特殊 class lines.push({ type: whitespace-change, ... }); }4.3 现象粘贴超长 base64 字符串10000 字符后页面卡死甚至崩溃原因highlightCode的正则在超长字符串上回溯爆炸catastrophic backtracking尤其.*类贪婪匹配。浏览器主线程被占满。解决对超长行200 字符跳过高亮直接原样输出function highlightCode(content) { if (content.length 200) return content; // 快速路 // ...原有高亮逻辑 }4.4 现象在 Chrome 115 中折叠块details点击无反应原因Chrome 115 默认启用了display: contents的新解析规则若父容器 CSS 有display: contents会破坏details的内置行为。解决强制重置details的 displaydetails { display: block; } details[open] summary::after { content: ▼; } details summary::after { content: ▶; }4.5 现象从 Excel 复制的表格数据粘贴后所有列挤在一行分隔符丢失原因Excel 复制的是富文本HTML 格式textarea.value只取纯文本但 Excel 的纯文本粘贴默认用 tab 分隔而非逗号或竖线。解决监听paste事件拦截并解析 clipboard 数据document.getElementById(left).addEventListener(paste, async (e) { e.preventDefault(); const text await navigator.clipboard.readText(); // 尝试按 tab 分割还原为多行 const lines text.split(\t).map(row row.trim()).filter(Boolean); document.getElementById(left).value lines.join(\n); });提示这些坑不是“理论上可能”而是我在某跨平台系统配置审计中被同一个tab分隔符问题折磨了两天后记下的。每一次翻车都是对“简易”二字的重新定义。5. 进阶技巧把简易 diff 页面变成团队标配工具——离线可用、一键导入、支持文件拖拽做到上一节你已经拥有了一个可靠的本地 diff 工具。但要让它真正“飞入寻常开发者家”还需三步① 确保离线绝对可用② 降低输入门槛不止是粘贴③ 支持常见工作流集成。下面是我给某公司内部知识库做的最终版增强全部基于标准 Web API无任何黑科技。5.1 打包为单 HTML 文件所有资源内联一个文件带走把 CSS 和 JS 全部内联进 HTML消除外部文件依赖实现真正的“下载即用”!-- index.html最终版 -- !DOCTYPE html html langzh-CN head meta charsetUTF-8 title简易文本比对工具类 git diff/title style /* 这里粘贴全部 style.css 内容 */ /style /head body !-- HTML 结构同 2.1 -- script // 这里粘贴全部 script.js 内容含 highlightCode、foldUnchangedBlocks 等 /script /body /html验证方法右键保存此 HTML 文件 → 断网 → 双击打开 → 输入两段文本 → 点击比对 → 功能完整。这才是“离线可用”的硬指标。文件大小约 12KB远小于一个 PNG 图片。5.2 支持文件拖拽上传直接拖 .json / .yaml / .sql 文件进来用户不想复制粘贴那就支持拖文件。监听drop事件读取文件内容// 追加到 script.js [left, right].forEach(id { const area document.getElementById(id); [dragenter, dragover, dragleave, drop].forEach(eventName { area.addEventListener(eventName, preventDefaults, false); }); function preventDefaults(e) { e.preventDefault(); e.stopPropagation(); } area.addEventListener(drop, handleDrop, false); function handleDrop(e) { const dt e.dataTransfer; const files dt.files; if (files.length) { const file files[0]; const reader new FileReader(); reader.onload (e) { const content e.target.result; area.value content; // 自动触发一次比对可选 // document.getElementById(diff-btn).click(); }; reader.readAsText(file); } } });参数说明readAsText(file)默认用 UTF-8 解码覆盖 99% 场景若需支持 GBK 等编码需用new TextDecoder(gbk)但会增大体积此处不展开。5.3 添加“一键导入当前页面源码”按钮比对网页前后端差异的神技前端改了模板后端改了接口返回如何快速确认 DOM 结构是否一致加一个按钮直接抓取当前页面html源码// 追加到 script.js document.getElementById(import-current).addEventListener(click, () { const html document.documentElement.outerHTML; document.getElementById(right).value html; // 自动聚焦右侧方便用户手动修改后比对 document.getElementById(right).focus(); });!-- 在 HTML 中添加按钮 -- button idimport-current stylemargin-left: 12px; 导入当前页 HTML/button场景价值某次排查 SSR 渲染差异时A同学把服务端吐出的 HTML 粘左B同学把浏览器渲染后的document.documentElement.outerHTML粘右一点比对立刻定位到是某个script标签未被服务端执行——这种“所见即所得”的比对是 CLI diff 永远做不到的。5.4 最终效果验证表你的 diff 页面是否达标检查项达标表现验证方式离线可用断网状态下双击 HTML 文件所有功能比对、折叠、高亮正常关闭 WiFi/拔网线双击运行中文兼容粘贴含中文、emoji、全角标点的文本行号准确、无乱码粘贴{状态: ✅运行中, 时间: 2024-06-15}大文件容忍粘贴 500 行 JSON约 80KB比对响应 800ms不卡顿用console.time()测量renderDiffHtml输入友好支持 CtrlV 粘贴、拖拽 .json 文件、点击按钮导入当前页 HTML三者逐一操作验证安全底线粘贴scriptalert(1)/script页面不弹窗内容原样显示为文本直接粘贴测试我一般会在交付前用这张表过一遍。不是为了炫技而是确保它真的能成为那个“谁都能用、在哪都能用、什么时候都敢用”的工具。而不是又一个躺在收藏夹吃灰的 demo。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询