Vue.js中展示完整对象:动态嵌套JSON树形目录展示与编辑问题
在Vue.js中实现动态可编辑的JSON树形展示
我之前也遇到过类似的需求,这种结构不固定、嵌套层级未知的JSON树形展示+编辑,核心思路就是用递归组件——让组件自己调用自己来渲染每一层节点。下面给你一套可直接复用的实现方案:
1. 核心递归组件实现
我们先写一个名为JsonTreeNode的递归组件,它会根据当前节点的类型(对象/普通值)来渲染不同的内容,同时支持展开/折叠和编辑功能。
Vue3 组合式API版本
<template> <div class="json-tree-node"> <!-- 如果是对象类型,渲染折叠/展开按钮和子节点 --> <div v-if="isObject(value)" class="node-header"> <button @click="toggleExpand" class="expand-btn"> {{ isExpanded ? '−' : '+' }} </button> <span class="node-key">{{ keyName }}</span> <!-- 展开状态下才渲染子节点 --> <div v-if="isExpanded" class="children"> <json-tree-node v-for="(childValue, childKey) in value" :key="childKey" :key-name="childKey" :value="childValue" :model-value="modelValue" @update:model-value="handleUpdate" /> </div> </div> <!-- 如果是普通值,渲染编辑输入框 --> <div v-else class="node-value"> <span class="node-key">{{ keyName }}</span> <input v-model="editableValue" @input="handleInput" class="edit-input" /> </div> </div> </template> <script setup> import { ref, computed } from 'vue' const props = defineProps({ keyName: { type: String, required: true }, value: { type: [Object, String, Number, Boolean], required: true }, modelValue: { type: Object, required: true } }) const emit = defineEmits(['update:modelValue']) // 记录当前节点的展开状态 const isExpanded = ref(false) // 判断当前值是否是对象(排除数组和null) const isObject = (val) => { return typeof val === 'object' && val !== null && !Array.isArray(val) } // 切换展开/折叠状态 const toggleExpand = () => { isExpanded.value = !isExpanded.value } // 处理普通值的编辑输入 const editableValue = computed({ get() { return props.value }, set(newVal) { handleInput(newVal) } }) const handleInput = (newVal) => { // 构造新对象保证响应式,递归更新对应属性 const updateNestedObj = (obj, key, newValue) => { if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) { return { ...obj, [key]: updateNestedObj(obj[key], key, newValue) } } else { return { ...obj, [key]: newValue } } } emit('update:modelValue', updateNestedObj(props.modelValue, props.keyName, newVal)) } // 接收子组件的更新事件并向上传递 const handleUpdate = (updatedModel) => { emit('update:modelValue', updatedModel) } </script> <style scoped> .json-tree-node { margin-left: 16px; } .node-header { display: flex; align-items: center; gap: 8px; cursor: pointer; } .expand-btn { width: 20px; height: 20px; padding: 0; border: none; background: #f0f0f0; border-radius: 4px; cursor: pointer; } .node-key { font-weight: 500; } .children { margin-top: 4px; } .node-value { display: flex; align-items: center; gap: 8px; } .edit-input { padding: 4px 8px; border: 1px solid #ddd; border-radius: 4px; min-width: 100px; } </style>
Vue2 选项式API版本(适配Vue2项目)
<template> <div class="json-tree-node"> <div v-if="isObject(value)" class="node-header"> <button @click="toggleExpand" class="expand-btn"> {{ isExpanded ? '−' : '+' }} </button> <span class="node-key">{{ keyName }}</span> <div v-if="isExpanded" class="children"> <json-tree-node v-for="(childValue, childKey) in value" :key="childKey" :key-name="childKey" :value="childValue" :model="model" @update="handleUpdate" /> </div> </div> <div v-else class="node-value"> <span class="node-key">{{ keyName }}</span> <input v-model="editableValue" @input="handleInput" class="edit-input" /> </div> </div> </template> <script> export default { name: 'JsonTreeNode', // 必须指定name才能递归调用 props: { keyName: { type: String, required: true }, value: { type: [Object, String, Number, Boolean], required: true }, model: { type: Object, required: true } }, data() { return { isExpanded: false, editableValue: this.value } }, methods: { isObject(val) { return typeof val === 'object' && val !== null && !Array.isArray(val) }, toggleExpand() { this.isExpanded = !this.isExpanded }, handleInput(newVal) { // Vue2中用$set保证响应式更新 const updatedModel = JSON.parse(JSON.stringify(this.model)) this.updateNestedObj(updatedModel, this.keyName, newVal) this.$emit('update', updatedModel) }, updateNestedObj(obj, key, newValue) { if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) { obj[key] = this.updateNestedObj(obj[key], key, newValue) } else { this.$set(obj, key, newValue) } return obj }, handleUpdate(updatedModel) { this.$emit('update', updatedModel) } }, watch: { value(newVal) { this.editableValue = newVal } } } </script> <style scoped> /* 样式和Vue3版本一致 */ .json-tree-node { margin-left: 16px; } .node-header { display: flex; align-items: center; gap: 8px; cursor: pointer; } .expand-btn { width: 20px; height: 20px; padding: 0; border: none; background: #f0f0f0; border-radius: 4px; cursor: pointer; } .node-key { font-weight: 500; } .children { margin-top: 4px; } .node-value { display: flex; align-items: center; gap: 8px; } .edit-input { padding: 4px 8px; border: 1px solid #ddd; border-radius: 4px; min-width: 100px; } </style>
2. 在父组件中使用递归组件
现在你可以在父组件中引入这个组件,把解析后的JSON对象传进去即可:
Vue3 父组件示例
<template> <div class="json-tree-container"> <h2>可编辑JSON树形展示</h2> <json-tree-node v-for="(value, key) in jsonData" :key="key" :key-name="key" :value="value" v-model="jsonData" /> <!-- 实时预览修改后的JSON --> <div class="preview"> <h3>当前JSON预览</h3> <pre>{{ JSON.stringify(jsonData, null, 2) }}</pre> </div> </div> </template> <script setup> import { ref } from 'vue' import JsonTreeNode from './JsonTreeNode.vue' // 替换成你解析后的JSON对象 const jsonData = ref({ buttons: { login: "LOGIN", register: "REGISTER", next: "Next" }, nav: { account: "Acount", login: "Login" } }) </script> <style scoped> .json-tree-container { padding: 20px; } .preview { margin-top: 20px; padding: 10px; background: #f5f5f5; border-radius: 8px; } pre { white-space: pre-wrap; word-wrap: break-word; } </style>
3. 关键细节说明
- 递归组件核心:Vue中组件必须指定
name属性(Vue3的<script setup>会自动推断文件名,但显式指定更稳妥),才能在组件内部调用自身。 - 响应式处理:Vue3中直接解构对象修改即可保持响应式;Vue2中需要用
this.$set修改对象属性,否则无法触发视图更新。 - 展开状态管理:每个节点独立维护展开/折叠状态,避免全局状态混乱,适配复杂嵌套结构。
- 扩展性:如果需要支持数组类型,只需修改
isObject方法添加数组判断,渲染时用数组索引作为key即可。
这样就能完美处理结构不固定、嵌套层级未知的JSON数据,实现树形展示和实时编辑功能了!
内容的提问来源于stack exchange,提问作者Michał Kruk
相关产品推荐
相关产品推荐

