json-diff-vue
v1.9.1
Published
JSON Diff Vue 组件库 - 同时支持 Vue2 和 Vue3
Maintainers
Readme
JSON Diff Vue
一个支持 Vue 2.0+ 和 Vue 3 的 JSON 差异展示组件库,提供直观的差异可视化展示。
特性
- 🎯 双版本支持 - 同时支持 Vue 2.0+ 和 Vue 3.x
- 📦 单包安装 - 一个 npm 包,按需导入对应版本
- 🎨 多种展示模式 - Normal(平铺)和 Tree(树形)两种模式
- 🔧 高度可配置 - 字段映射、统计栏、工具栏、展开控制
- 💪 TypeScript 支持 - 完整的类型定义
- 🚀 零依赖核心 - 基于 json-diff-toolkit,无额外运行时依赖
安装
npm install json-diff-vue
json-diff-toolkit是json-diff-vue的依赖,会随json-diff-vue自动安装,无需单独安装。
快速开始
Vue 3
// main.ts
import { createApp } from 'vue'
import JsonDiffVue from 'json-diff-vue/vue3'
import 'json-diff-vue/style.css'
createApp(App).use(JsonDiffVue).mount('#app')Vue 2
// main.ts
import Vue from 'vue'
import JsonDiffVue from 'json-diff-vue/vue2'
import 'json-diff-vue/style.css'
Vue.use(JsonDiffVue)按需导入
import { DiffViewer } from 'json-diff-vue/vue3' // Vue 3
import { DiffViewer } from 'json-diff-vue/vue2' // Vue 2基本用法
<template>
<DiffViewer :json1="before" :json2="after" />
</template>
<script setup>
const before = { name: '张三', age: 18 }
const after = { name: '张三', age: 19, city: '北京' }
</script>展示模式
Normal 模式(默认)
以列表形式平铺展示所有差异项,每条差异显示路径、类型及新旧值。
<DiffViewer :json1="a" :json2="b" mode="normal" />Tree 模式
以树形结构还原 JSON 层级,支持展开/折叠,适合深层嵌套的复杂数据。
<DiffViewer :json1="a" :json2="b" mode="tree" :default-expand-all="true" />Props
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
| json1 | unknown | — | 必填,原始 JSON |
| json2 | unknown | — | 必填,对比 JSON |
| mode | 'normal' \| 'tree' | 'normal' | 展示模式 |
| config | DiffConfig | {} | 差异检测配置(见下方) |
| maxHeight | number \| string | — | 容器最大高度,超出后滚动 |
| customClass | string | — | 根元素附加 CSS 类名 |
| showStats | boolean | true | 是否显示统计信息栏 |
| statsConfig | StatsDisplayConfig | {} | 统计栏显示项配置(见下方) |
| fieldMapping | Record<string, string> | {} | 字段路径 → 显示名称映射(见下方) |
| showToolbar | boolean | true | Tree 模式:是否显示展开/折叠工具栏 |
| defaultExpandAll | boolean | true | Tree 模式:初始展开所有节点 |
| defaultExpandedKeys | string[] | [] | Tree 模式:初始展开的节点路径列表 |
| truncate | boolean | false | 内容超长时启用省略号截断;默认 false 全量展示 |
| showRawData | boolean | false | 是否在差异视图上方展示原始 JSON 数据面板 |
| fieldMapper | FieldMapper | — | 动态字段映射函数,优先级高于 fieldMapping(见下方) |
| htmlKeys | boolean | false | Tree 模式:以 v-html 渲染映射后的键名,支持 HTML 标签(见下方) |
| hideNodes | string[] | [] | 隐藏指定节点行;支持精确路径、标准化路径、*.fieldName;Tree 模式子节点仍渲染(见下方) |
| shouldHideNode | NodeHideFilter | — | Tree 模式节点隐藏回调,返回 true 则隐藏节点行,子节点仍渲染 |
| shouldHideDiff | DiffHideFilter | — | Normal 模式差异行隐藏回调,返回 true 则隐藏该行 |
暴露方法
通过 ref 可在外部控制 Tree 模式的展开状态:
<DiffViewer ref="viewer" mode="tree" :json1="a" :json2="b" />
<button @click="viewer.expandAll()">全部展开</button>
<button @click="viewer.collapseAll()">全部折叠</button>// Vue 3 script setup
const viewer = ref()
viewer.value.expandAll()
viewer.value.collapseAll()// Vue 2 Options API
this.$refs.viewer.expandAll()
this.$refs.viewer.collapseAll()DiffConfig 差异检测配置
interface DiffConfig {
ignoreFields?: string[] // 忽略字段,支持精确路径、通配符、前缀、*.fieldName 四种写法(见下方)
ignoreNull?: boolean // 忽略 null ↔ 非null 的变化,默认 false
deepEqual?: boolean // 深度对比对象/数组,默认 true(false 时用 JSON.stringify 浅比较)
arrayIdConfig?: Record<string, string> // 对象数组按 ID 字段匹配(见下方)
arrayCompareMode?: Record<string, // 数组对比策略(见下方),默认 lcs
'lcs' | 'index' | { strategy: 'index', matchKeys?: string[] }>
maxDepth?: number // 最大对比深度,默认无限制
}注意:
DiffConfig.format.*(如format.style: 'arrow')为json-diff-toolkit文本格式化 API 的配置项,仅在直接调用formatDiff()/JsonDiffToolkit类时有效。DiffViewer组件自行以 HTML 方式渲染差异,不使用文本格式化器,传入format.*不会生效。
ignoreFields 忽略字段
支持四种匹配模式,对含运行时数组标记([id=xxx]、[0])的路径自动做标准化后再匹配,无需写死 ID:
| 模式 | 示例配置 | 效果 |
|---|---|---|
| 精确路径 | "user.name" | 仅忽略该字段,其他字段不受影响 |
| 后缀通配符 .* | "user.*" | 忽略该节点的所有后代(不含节点本身) |
| 前缀路径 | "user" | 忽略该节点本身及所有后代 |
| 全局字段名 *.<name> | "*.uTime" | 忽略任意深度的同名字段 |
<DiffViewer
:json1="a"
:json2="b"
:config="{
ignoreFields: [
'updatedAt', // 精确路径:忽略顶层 updatedAt 字段
'meta.updatedAt', // 精确路径:仅忽略 meta 下的 updatedAt
'meta.*', // 后缀通配符:忽略 meta 下所有字段(含深层后代)
'meta', // 前缀路径:忽略 meta 节点本身及全部后代
'*.uTime', // 全局字段名:忽略任意深度的 uTime 字段
]
}"
/>在对象数组中使用(自动标准化,无需写死 ID):
<DiffViewer
:json1="a"
:json2="b"
:config="{
arrayIdConfig: { nodeList: 'id' },
ignoreFields: [
'nodeList.uTime',
// 匹配 nodeList[id=48488].uTime、nodeList[id=99].uTime 等所有元素的 uTime
'nodeList.nodeApprover.nodeApprovers.uTime',
// 匹配深层嵌套路径 nodeList[id=48488].nodeApprover.nodeApprovers[id=74327].uTime
'nodeList.*',
// 忽略 nodeList 所有元素的全部字段(nodeList 被新增/删除时仍会上报)
'nodeList',
// 忽略 nodeList 节点本身及全部后代(包含新增/删除整个 nodeList 的情况)
]
}"
/>.* 与前缀路径的区别:
| 配置 | 节点本身被新增/删除 | 字段修改 |
|---|---|---|
| "nodeList.*" | ✅ 上报 | ❌ 忽略 |
| "nodeList" | ❌ 忽略 | ❌ 忽略 |
arrayIdConfig 对象数组 ID 匹配
默认情况下数组使用 LCS(最长公共子序列)算法对比,无法感知元素身份,相同元素的修改可能被识别为"删除旧元素 + 新增新元素"。配置 arrayIdConfig 后,引擎按指定 ID 字段匹配同一元素,正确识别新增、删除、修改。
<DiffViewer
:json1="a"
:json2="b"
:config="{
arrayIdConfig: {
users: 'id', // 顶层数组 users,按元素的 id 字段匹配
'dept.members': 'userId', // 嵌套数组,按 userId 字段匹配
}
}"
/>深层嵌套数组(key 支持标准化路径,自动命中含运行时 ID 的中间层):
<DiffViewer
:json1="a"
:json2="b"
:config="{
arrayIdConfig: {
nodeList: 'id',
// 配置 nodeList 下的嵌套数组
// 实际运行时路径为 nodeList[id=48488].nodeApprover.nodeApprovers
// 无需写死 ID,引擎自动标准化路径后匹配
'nodeList.nodeApprover.nodeApprovers': 'id',
}
}"
/>数组元素路径格式:配置 arrayIdConfig 后,diff 路径由索引格式变为 ID 格式:
未配置:nodeList[0].uTime、nodeList[1].uTime
已配置:nodeList[id=48488].uTime、nodeList[id=68523].uTimeID 格式路径可被 ignoreFields 和 fieldMapping 的标准化路径直接命中,三者协同工作。
arrayCompareMode 数组对比策略
当 arrayIdConfig 未命中时,通过 arrayCompareMode 指定数组对比策略。key 写法与 arrayIdConfig 相同,支持精确路径、标准化路径、字段名兜底和 "*" 全局默认四级查找。
| 策略 | 说明 |
|---|---|
| 'lcs' | 最长公共子序列算法(默认),能识别元素顺序变化 |
| 'index' | 按索引一一对比,适合顺序固定的数组 |
| { strategy: 'index', matchKeys: [...] } | 按索引对比,同时用指定字段验证同索引元素是否为同一实体;字段值不全相等时整体视为 removed + added |
<DiffViewer
:json1="a"
:json2="b"
:config="{
arrayCompareMode: {
'rows': { strategy: 'index', matchKeys: ['type'] },
// rows[i].type 相同 → 逐字段对比;不同 → 整体替换
'cells': { strategy: 'index', matchKeys: ['type', 'colSpan'] },
// 同时验证 type 和 colSpan 两个字段
'tags': 'index',
// 按索引对比,不做实体验证
'*': 'lcs',
// 其他数组默认 LCS(可省略,lcs 是默认值)
}
}"
/>StatsDisplayConfig 统计栏配置
控制统计栏展示哪些项目以及标签文字:
interface StatsDisplayConfig {
showAdded?: boolean // 显示新增数,默认 true
showRemoved?: boolean // 显示删除数,默认 true
showModified?: boolean // 显示修改数,默认 true
showTotal?: boolean // 显示差异总计,默认 false
showMaxDepth?: boolean // 显示最大深度,默认 false
labels?: {
added?: string // 默认 '新增'
removed?: string // 默认 '删除'
modified?: string // 默认 '修改'
total?: string // 默认 '总计'
maxDepth?: string // 默认 '最大深度'
}
}示例:
<DiffViewer
:json1="a"
:json2="b"
:stats-config="{
showTotal: true,
labels: { added: 'Added', removed: 'Removed', modified: 'Changed' }
}"
/>fieldMapping 字段映射
将字段路径映射为业务可读的显示名称,支持三级查找优先级:
| 优先级 | key 写法 | 说明 |
|---|---|---|
| ① 精确路径 | "users[id=1].name" | 完全匹配含 ID 的实际路径,优先级最高 |
| ② 作用域路径 | "users.name" | 去掉 [...] 后的标准化路径,匹配该上下文内的字段 |
| ③ 字段名 | "name" | 全局匹配任意路径中同名字段 |
<DiffViewer
:json1="a"
:json2="b"
:field-mapping="{
// ③ 字段名级:全局映射,任意路径下的 uTime 均生效
uTime: '更新时间',
cTime: '创建时间',
// ② 作用域路径级:仅在 nodeList 下的 uTime 使用此名称(覆盖字段名级)
'nodeList.uTime': '节点更新时间',
// ② 多级作用域:仅在该嵌套路径下的 uTime 使用此名称(覆盖上层)
'nodeList.nodeApprover.nodeApprovers.uTime': '审批人更新时间',
// ② 数组名称映射:[id=xxx] 标记自动保留
nodeList: '节点列表', // 显示为 节点列表[id=48488]
nodeLinkList: '连接线列表',
}"
/>实际路径 → 显示结果示例
| 实际路径 | 显示结果 |
|---|---|
| nodeList[id=48488].uTime | 节点列表[id=48488].节点更新时间 |
| nodeList[id=48488].nodeApprover.nodeApprovers[id=74327].uTime | 节点列表[id=48488].nodeApprover.nodeApprovers[id=74327].审批人更新时间 |
| nodeLinkList[id=68523].uTime | 连接线列表[id=68523].更新时间(无 nodeLinkList.uTime 配置,回退到字段名级) |
fieldMapper 动态字段映射
fieldMapper 是一个函数 prop,接收字段路径、差异结果和可选的上下文,返回自定义显示名称,优先级高于静态 fieldMapping。适用于需要根据运行时上下文(差异类型、值内容、父级对象字段等)动态决定显示名称的场景。
type FieldMapper = (
path: string,
diff: DiffResult,
context?: FieldMapperContext
) => string | undefined | null
interface FieldMapperContext {
currentOldValue: unknown // 当前路径对应的旧值(当前字段/节点本身)
currentNewValue: unknown // 当前路径对应的新值
parentOldValue: unknown // 父路径对应的旧值(包含当前字段的父对象)
parentNewValue: unknown // 父路径对应的新值
rootOld: unknown // 完整的旧 JSON 根对象
rootNew: unknown // 完整的新 JSON 根对象
}import type { FieldMapper, FieldMapperContext } from 'json-diff-vue/vue3'
import type { DiffResult } from 'json-diff-toolkit'示例一:基础路径映射
用函数替代静态 fieldMapping 对象,实现路径 → 显示名称的动态转换:
const pathMap: Record<string, string> = {
userId: '用户ID',
userName: '用户姓名',
deptCode: '部门编码',
statusCode: '账号状态',
'profile.phone': '手机号',
'profile.email': '邮箱',
remark: '备注',
}
const basicMapper: FieldMapper = (path) => pathMap[path] ?? null<DiffViewer :json1="a" :json2="b" :field-mapper="basicMapper" />示例二:基于差异类型附加状态标记
利用第二个参数 diff,根据差异类型(added / removed / modified)在字段名后动态附加状态标记:
const typeAwareMapper: FieldMapper = (path: string, diff: DiffResult) => {
const name = pathMap[path]
if (!name) return null
const tag = diff.type === 'added' ? '(新增)'
: diff.type === 'removed' ? '(删除)'
: null
return tag ? name + tag : name
}<DiffViewer :json1="a" :json2="b" :field-mapper="typeAwareMapper" />示例三:fieldMapper 与 fieldMapping 协同使用
fieldMapper 只处理部分路径,其余路径返回 null 自动回退到 fieldMapping,两者互补:
// fieldMapper 只覆盖 profile 下的字段
const profileMapper: FieldMapper = (path) => {
const map: Record<string, string> = {
'profile.phone': '📱 手机号',
'profile.email': '📧 邮箱地址',
'profile.address': '📍 所在城市',
}
return map[path] ?? null
}
// 其余字段由 fieldMapping 接管
const fieldMapping = {
userId: '用户ID',
userName: '用户姓名',
deptCode: '部门编码',
statusCode: '账号状态',
}<DiffViewer
:json1="a"
:json2="b"
:field-mapper="profileMapper"
:field-mapping="fieldMapping"
/>示例四:利用 context 访问当前节点 / 父对象数据
context 由组件在调用 toolkit 时自动注入,无需额外配置。常见用途:
context.currentOldValue— 当前路径对应的值本身(如nodeList[id=38945]路径下就是该节点对象)context.parentOldValue— 父对象(如nodeList[id=38945].uTime路径下父对象是该节点,可读取.name等兄弟字段)
const nodeMapper: FieldMapper = (path, diff, context) => {
// 将数组元素节点显示为其 name 字段
if (path.match(/^nodeList\[/) && path.endsWith(']')) {
const node = context?.currentOldValue as Record<string, unknown> | undefined
return node?.name as string | undefined
}
// 将子字段路径追加父节点 name 作为前缀
if (path.match(/^nodeList\[.+\]\.uTime$/)) {
const parent = context?.parentOldValue as Record<string, unknown> | undefined
return parent?.name ? `${parent.name} - 更新时间` : '更新时间'
}
return undefined
}<DiffViewer
:json1="a"
:json2="b"
:field-mapper="nodeMapper"
:field-mapping="fieldMapping"
/>回退优先级
| 优先级 | 来源 | 触发条件 |
|---|---|---|
| ① | fieldMapper 返回值 | 返回非空字符串时生效 |
| ② | fieldMapping 静态映射 | fieldMapper 返回 null/undefined 时回退 |
| ③ | 原始字段名 | 两者均未命中时使用 |
htmlKeys 键名 HTML 渲染
仅 Tree 模式生效
默认情况下,所有键名均以纯文本渲染。开启 htmlKeys 后,fieldMapping 的值和 fieldMapper 的返回值会以 v-html 渲染,可在键名中嵌入 HTML 标签(加粗、着色、角标等)。
注意:
htmlKeys仅影响通过fieldMapping/fieldMapper映射后的键名;未命中任何映射的原始 JSON key 始终以纯文本渲染(规避 XSS)- HTML 内容由开发者在配置中提供,确保来源可控
示例:加粗显示部分字段
<DiffViewer
mode="tree"
:json1="a"
:json2="b"
:html-keys="true"
:field-mapping="{
name: '<b>姓名</b>',
status: '<span style=\"color:#409eff\">状态</span>',
price: '价格'
}"
/>效果:键名 name 显示为加粗「姓名」,status 显示为蓝色「状态」,price 显示为普通文本「价格」。
示例:与 fieldMapper 结合
fieldMapper 返回的字符串同样支持 HTML,配合 htmlKeys 使用:
const fieldMapper = (path, diff) => {
if (diff.type === 'added') return `<span style="color:#67c23a">${path.split('.').pop()}</span>`
if (diff.type === 'removed') return `<span style="color:#f56c6c">${path.split('.').pop()}</span>`
return null // 其他字段回退到 fieldMapping
}<DiffViewer
mode="tree"
:json1="a"
:json2="b"
:html-keys="true"
:field-mapper="fieldMapper"
:field-mapping="{ name: '姓名', age: '年龄' }"
/>节点行隐藏
通过 hideNodes、shouldHideNode(Tree 模式)、shouldHideDiff(Normal 模式)可在渲染阶段隐藏指定节点,不影响底层 diff 计算结果和统计数量。
Tree 模式:隐藏节点行,子节点上浮
被隐藏的容器节点不渲染行,其子节点提升一级直接显示(始终展开,不受折叠状态影响):
隐藏前 隐藏后(hideNodes: ['user'])
▼ user {...} name: Alice → Bob
name: Alice → Bob → age: 25 → 26
age: 25 → 26hideNodes 路径匹配规则(与 ignoreFields 一致):
| 写法 | 效果 |
|---|---|
| "user" | 精确匹配路径 user |
| "nodeList.info" | 标准化匹配,可命中 nodeList[id=1].info |
| "*.uTime" | 全局字段名,匹配任意深度的 uTime 节点 |
<DiffViewer
mode="tree"
:json1="a"
:json2="b"
:hide-nodes="['wrapper', '*.metadata']"
/>Tree 模式:回调函数控制
shouldHideNode 可根据节点任意属性动态决定是否隐藏:
<script>
// 隐藏所有未发生变化的容器节点(diffType 为 null)
const shouldHideNode = (node) => node.nodeType !== 'value' && !node.diffType
</script>
<DiffViewer
mode="tree"
:json1="a"
:json2="b"
:should-hide-node="shouldHideNode"
/>Normal 模式:隐藏差异行
hideNodes 和 shouldHideDiff 在 Normal 模式下隐藏整条差异行:
<script>
import type { DiffHideFilter } from 'json-diff-vue/vue3'
// 隐藏所有时间戳字段
const shouldHideDiff: DiffHideFilter = (diff) => diff.path.endsWith('Time')
</script>
<DiffViewer
mode="normal"
:json1="a"
:json2="b"
:hide-nodes="['*.uTime', '*.createTime']"
:should-hide-diff="shouldHideDiff"
/>
hideNodes与回调同时生效,任意一个命中即隐藏。
Tree 模式工具栏控制
隐藏内置工具栏,完全由外部控制展开/折叠:
<DiffViewer
ref="viewer"
mode="tree"
:show-toolbar="false"
:json1="a"
:json2="b"
/>
<button @click="$refs.viewer.expandAll()">全部展开</button>
<button @click="$refs.viewer.collapseAll()">全部折叠</button>指定默认展开的路径:
<DiffViewer
mode="tree"
:default-expanded-keys="['user', 'user.address']"
:json1="a"
:json2="b"
/>内容截断
默认全量展示所有内容。设置 truncate 可对超长字段启用省略号截断:
<DiffViewer :json1="a" :json2="b" :truncate="true" />截断范围:Normal 模式值字段(max-width: 300px)、Tree 模式键名(200px)及旧值/新值(240px)。
原始数据展示
设置 showRawData 可在差异视图上方展示双列原始 JSON 面板(左列原始数据、右列修改后数据):
<DiffViewer :json1="a" :json2="b" :show-raw-data="true" />raw-data 插槽
通过 raw-data 具名插槽可完全自定义原始数据的展示方式,插槽通过 slot props 暴露 json1 和 json2:
<DiffViewer :json1="a" :json2="b" :show-raw-data="true">
<template #raw-data="{ json1, json2 }">
<div class="my-raw-panel">
<textarea :value="JSON.stringify(json1, null, 2)" readonly />
<textarea :value="JSON.stringify(json2, null, 2)" readonly />
</div>
</template>
</DiffViewer>插槽仅在
showRawData="true"时渲染。提供插槽内容后,内置的双列 JSON 面板会被替换。
样式定制
所有内置 CSS 类名均以 jdv- 为前缀(BEM 规范),可按需覆盖:
/* 修改差异行背景色 */
.jdv-node__row--modified { background: #fff8e1; }
.jdv-normal__item--added { border-left-color: #4caf50; }
/* 修改统计栏背景 */
.jdv-stats { background: #f0f4ff; }通过 customClass 附加类名以限定作用域:
<DiffViewer custom-class="my-diff" :json1="a" :json2="b" />.my-diff .jdv-node__key { color: #1a1a2e; }TypeScript
import type { DiffViewerProps, DiffStats, StatsDisplayConfig } from 'json-diff-vue/vue3'兼容性
- Vue 3.x
- Vue 2.0+(所有 Vue 2.x 版本,无需 Composition API)
- 现代浏览器(Chrome / Firefox / Safari / Edge)
打包工具兼容性
| 打包工具 | 支持情况 | |---|---| | Vite 3+ | ✅ | | webpack 5 | ✅ | | webpack 4(Vue CLI 4.x) | ✅ | | Rollup | ✅ |
webpack 4 说明: webpack 4 不支持 package.json 的 exports 字段。本包提供了物理代理文件(vue2/index.js、vue3/index.js、style.css),webpack 4 会自动通过这些文件加载正确的产物,无需任何额外配置。
更新日志
查看 CHANGELOG.md 了解版本更新历史。
相关项目
- json-diff-toolkit - 核心差异检测库
