XML字符串对比,按XML绝对路径(如root.xxx.xx.xx)忽略对比特定行,不改变原数据,完整渲染XML,仅在展示diff时,特定路径的行不高亮。
- 已尝试diff和diff2html库
- 目前采用暴力删除CSS方案
- 寻求更优雅的技术实现方法
目前自己的实现
<!-- App.vue -->
<template>
<div>
<diff-viewer
:old-text="parseXmlStructure(oldXml)"
:new-text="parseXmlStructure(newXml)"
:ignore-paths="[
'root.company.gcc',
'root.company.employees.employee.salary',
]"
file-name="config.xml"
@diff-rendered="onDiffRendered"
/>
</div>
</template>
<script>
import DiffViewer from './components/diff2html.vue';
import vkbeautify from 'vkbeautify';
export default {
components: {
DiffViewer,
},
data() {
return {
oldXml: `<root>
<company id="123">
<gcc/>
<name>TechCorp International</name>
<foundedYear>1995</foundedYear>
<employees>
<employee id="1">
<name>John Smith</name>
<position>Senior Developer</position>
<department>Engineering</department>
<salary>85000</salary>
<contact>
<email>john.smith@techcorp.com</email>
<phone>212-555-0100</phone>
<address>
<street>123 Tech Avenue</street>
<city>San Francisco</city>
<state>CA</state>
<country>USA</country>
<zipCode>94105</zipCode>
</address>
</contact>
<projects>
<project>Cloud Migration</project>
<project>Mobile App</project>
</projects>
</employee>
<employee id="2">
<name>Sarah Johnson</name>
<position>Product Manager</position>
<department>Product</department>
<salary>95000</salary>
<contact>
<email>sarah.j@techcorp.com</email>
<phone>212-555-0101</phone>
<address>
<street>456 Innovation Drive</street>
<city>San Francisco</city>
<state>CA</state>
<country>USA</country>
<zipCode>94105</zipCode>
</address>
</contact>
<projects>
<project>User Analytics</project>
<project>Platform Redesign</project>
</projects>
</employee>
</employees>
<offices>
<office>
<location>San Francisco</location>
<employeeCount>150</employeeCount>
</office>
<office>
<location>New York</location>
<employeeCount>75</employeeCount>
</office>
</offices>
</company>
</root>`,
newXml: `<root>
<company id="123">
<name>TechCorp International</name>
<foundedYear>1995</foundedYear>
<employees>
<employee id="1">
<name>John Smith</name>
<position>Engineering Manager</position>
<department>Engineering</department>
<salary>95000</salary>
<contact>
<email>john.smith@techcorp.com</email>
<phone>212-555-0100</phone>
<address>
<street>123 Tech Avenue</street>
<city>San Jose</city>
<state>CA</state>
<country>USA</country>
<zipCode>95110</zipCode>
</address>
</contact>
<projects>
<project>Cloud Migration</project>
<project>Mobile App</project>
<project>AI Integration</project>
</projects>
</employee>
<employee id="2">
<name>Sarah Johnson</name>
<position>Director of Product</position>
<department>Product</department>
<salary>120000</salary>
<contact>
<email>sarah.johnson@techcorp.com</email>
<phone>212-555-0202</phone>
<address>
<street>456 Innovation Drive</street>
<city>San Jose</city>
<state>CA</state>
<country>USA</country>
<zipCode>95110</zipCode>
</address>
</contact>
<projects>
<project>User Analytics 2.0</project>
<project>Platform Redesign</project>
<project>Customer Portal</project>
</projects>
</employee>
</employees>
<offices>
<office>
<location>San Jose</location>
<employeeCount>200</employeeCount>
</office>
<office>
<location>New York</location>
<employeeCount>80</employeeCount>
</office>
</offices>
</company>
</root>`,
};
},
methods: {
onDiffRendered() {
console.log('Diff rendering completed');
},
parseXmlStructure(content) {
try {
// 移除多余的空白行
content = content.replace(/^\s*[\r\n]/gm, '');
// 格式化XML
return vkbeautify.xml(content, 2);
} catch (error) {
console.error('XML formatting error:', error);
return content;
}
},
},
};
</script>
<!-- DiffViewer.vue -->
<template>
<div class="diff-viewer">
<div v-if="loading" class="diff-loading">
<div class="loading-spinner"></div>
</div>
<div v-else-if="error" class="diff-error">
{{ error }}
<button @click="retry" class="retry-btn">重试</button>
</div>
<div
v-else
ref="diffContainer"
class="diff-container"
v-html="diffHtml"
></div>
<div class="diff-controls">
<label>
<input type="checkbox" v-model="sideBySide" @change="redraw" />
并排显示
</label>
<select v-model="matching" @change="redraw">
<option value="lines">按行匹配</option>
<option value="words">按词匹配</option>
<option value="none">禁用匹配</option>
</select>
</div>
</div>
</template>
<script>
import 'diff2html/bundles/css/diff2html.min.css';
import { createPatch } from 'diff';
import { parse, html } from 'diff2html';
export default {
name: 'DiffViewer',
props: {
oldText: {
type: String,
required: true,
},
newText: {
type: String,
required: true,
},
fileName: {
type: String,
default: 'file.txt',
},
ignorePaths: {
type: Array,
default: () => [],
},
},
data() {
return {
loading: false,
error: null,
sideBySide: true,
matching: 'lines',
diffHtml: '',
};
},
watch: {
oldText: {
handler: 'updateDiff',
immediate: true,
},
newText: {
handler: 'updateDiff',
immediate: true,
},
},
methods: {
async updateDiff() {
try {
this.loading = true;
this.error = null;
// 生成diff
const diffStr = createPatch(
this.fileName,
this.oldText,
this.newText,
'',
'',
{ context: 3 }
);
// 解析diff
const diffJson = parse(diffStr);
// 配置选项
const config = {
drawFileList: false,
matching: this.matching,
outputFormat: this.sideBySide ? 'side-by-side' : 'line-by-line',
renderNothingWhenEmpty: true,
};
// 生成 HTML
this.diffHtml = html(diffJson, config);
// 等待 DOM 更新后处理 XML diff
this.$nextTick(() => {
this.processXmlDiff();
this.$emit('diff-rendered');
});
} catch (err) {
console.error('Diff generation failed:', err);
this.error = '差异对比生成失败,请重试';
} finally {
this.loading = false;
}
},
retry() {
this.updateDiff();
},
redraw() {
this.updateDiff();
},
parseXmlLine(line) {
const result = {
openTags: [], // 此行打开的标签
closeTags: [], // 此行关闭的标签
selfClosing: [], // 自闭合标签
content: null, // 内容
isContentLine: false, // 是否是内容行
};
if (!line || !line.includes('<')) {
result.content = line ? line.trim() : '';
result.isContentLine = true;
return result;
}
const tagPattern = /<\/?([^\s>]+)[^>]*\/?>/g;
let match;
let lastIndex = 0;
while ((match = tagPattern.exec(line)) !== null) {
const fullTag = match[0];
const tagName = match[1];
if (match.index > lastIndex) {
const content = line.substring(lastIndex, match.index).trim();
if (content) {
result.content = content;
result.isContentLine = true;
}
}
if (fullTag.endsWith('/>')) {
result.selfClosing.push(tagName.replace(/\/$/, ''));
} else if (fullTag.startsWith('</')) {
result.closeTags.push(tagName);
} else {
result.openTags.push(tagName);
}
lastIndex = match.index + fullTag.length;
}
if (lastIndex < line.length) {
const content = line.substring(lastIndex).trim();
if (content) {
result.content = content;
result.isContentLine = true;
}
}
return result;
},
shouldIgnorePath(path) {
if (!path) return false;
return this.ignorePaths.some((ignorePath) => {
const pattern = ignorePath.replace(/\*/g, '[^.]+');
const regex = new RegExp(`^${pattern}$`);
return regex.test(path);
});
},
removeHighlightStyle(row) {
const lineNumCell = row.querySelector('.d2h-code-side-linenumber');
const codeCell = row.querySelector('.d2h-del, .d2h-ins');
if (!lineNumCell || !codeCell) return;
lineNumCell.classList.remove('d2h-del', 'd2h-ins', 'd2h-change');
lineNumCell.classList.add('d2h-cntx');
codeCell.classList.remove('d2h-del', 'd2h-ins', 'd2h-change');
codeCell.classList.add('d2h-cntx');
const prefix = codeCell.querySelector('.d2h-code-line-prefix');
if (prefix) {
prefix.textContent = ' ';
}
const container = codeCell.querySelector('.d2h-code-line-ctn');
if (container) {
const delTags = container.querySelectorAll('del');
const insTags = container.querySelectorAll('ins');
delTags.forEach((del) => {
const text = del.textContent;
del.replaceWith(text);
});
insTags.forEach((ins) => {
const text = ins.textContent;
ins.replaceWith(text);
});
}
},
processXmlDiff() {
const diffContainer = this.$refs.diffContainer;
if (!diffContainer) return;
const tables = diffContainer.querySelectorAll('.d2h-diff-table');
tables.forEach((table) => {
const rows = table.querySelectorAll('tr');
const currentPath = [];
rows.forEach((row) => {
const contentCell = row.querySelector('.d2h-code-line-ctn');
if (!contentCell) return;
const line = contentCell.textContent;
const xmlInfo = this.parseXmlLine(line);
// 更新当前路径
xmlInfo.openTags.forEach((tag) => currentPath.push(tag));
// 处理自闭合标签
xmlInfo.selfClosing.forEach((tag) => {
currentPath.push(tag);
if (this.shouldIgnorePath(currentPath.join('.'))) {
this.removeHighlightStyle(row);
}
currentPath.pop();
});
// 处理内容行
if (
xmlInfo.isContentLine &&
this.shouldIgnorePath(currentPath.join('.'))
) {
this.removeHighlightStyle(row);
}
// 处理关闭标签
xmlInfo.closeTags.forEach(() => currentPath.pop());
});
});
},
},
};
</script>
"diff": "^7.0.0",
"diff2html": "^3.4.48",
"vkbeautify": "^0.99.3"