json-diff-vue

JSON Diff Vue 组件库 - 同时支持 Vue2 和 Vue3
3,565
Install
npm install json-diff-vue
Latest Version:1.9.1
Vue:2 · 3
License:MIT
Last Updated:May 18, 2026
Links: npm
Maintainer: zqlei

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].uTime

ID 格式路径可被 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 → 26

hideNodes 路径匹配规则(与 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 了解版本更新历史。

相关项目

Related Packages

vue-json-compare

A vue(2.x) components for compare JSON data

210,787
vue-json-diff

A json diff vue component

2,248 31
json-diff-viewer-component

Vanilla JS web component for side-by-side JSON diff visualization

16,748 47
@netilon/differify

Differify allows you to get the diff between two entities (objects diff, arrays...

21,268 29

Version History

Version Released Vue License
1.9.1 ^2.0.0 || ^3.0.0 MIT
1.8.2 ^2.0.0 || ^3.0.0 MIT
1.8.1 ^2.0.0 || ^3.0.0 MIT
1.7.0 ^2.0.0 || ^3.0.0 MIT
1.6.2 ^2.0.0 || ^3.0.0 MIT