从0-1实现一个G Json View组件。
- 第一版
扁平化处理后渲染。
- 第二版
递归渲染 G Json View 【二】 - 掘金 (juejin.cn)。
第一版
扁平化处理后渲染。
分析JSON格式
JSON内容可分为:对象开始、对象结束、数组开始、数组结束、纯文本;
分类定义为:'objectStart'、'objectEnd'、'arrayStart'、'arrayEnd'、'content';
将JSON数据扁平化处理
将JSON数据扁平化处理,打上内容分类的标记;
只有对象的纯文本类型时有key值,其他没有;
export function getType(value) {
return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
}
// 将JSON数据扁平化处理,打上内容分类的标记
export function jsonFlatten(
// json数据
data,
// 键值
key = null,
// 层级
level = 0,
// 是否显示逗号
showComma = false,
// 路径
path = 'root',
// 是否展开
isOpen = true,
) {
// 获取数据类型
const dataType = getType(data);
if (dataType === 'array') {
// 数组类型
// 递归
const newData = data.map((item, ind) => {
return jsonFlatten(item, null, level + 1, ind !== data.length - 1, `${path}[${ind}]`, isOpen);
});
// 扁平化数组中间部分
const result = flattenArray(newData);
// 添加数组开始
result.unshift({
content: '[',
key,
type: 'arrayStart',
level,
showComma: false,
path,
isOpen,
});
// 添加数组结束
result.push({
content: ']',
// 数组/对象结束标识不显示键值
key: null,
type: 'arrayEnd',
level,
showComma: true,
path,
isOpen,
});
return result;
} else if (dataType === 'object') {
// 对象类型
const keys = Object.keys(data);
// 递归
const newData = keys.map((objKey, ind) => {
return jsonFlatten(data[objKey], objKey, level + 1, ind !== keys.length - 1, `${path}[${ind}]`, isOpen);
});
const result = flattenArray(newData);
// 添加对象开始
result.unshift({
content: '{',
key,
type: 'objectStart',
level,
showComma: false,
path,
isOpen,
});
// 添加对象结束
result.push({
content: '}',
// 数组/对象结束标识不显示键值
key: null,
type: 'objectEnd',
level,
showComma,
path,
isOpen,
});
return result;
}
return [
{
content: data,
key,
type: 'content',
level,
showComma,
path,
isOpen,
},
];
}
export function flattenArray(arr) {
if (typeof Array.prototype.flat === 'function') {
return arr.flat();
}
// 创建一个空数组,用于存储扁平化后的结果
let result = [];
for (let i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
// 如果当前元素是一个数组,就将它里面的元素全部扁平化,并加入结果数组
result = result.concat(flattenArray(arr[i]));
} else {
// 如果当前元素不是一个数组,就直接加入结果数组
result.push(arr[i]);
}
}
// 返回扁平化后的数组
return result;
}
渲染
将扁平化处理后的JSON数据按标记渲染;
优化
增加层级标识,分层级缩进;
之前版本逗号显示有问题,修改;
纯文本最后一个元素和整体最后一个元素不加逗号;
增加双引号;
纯文本样式:字符串、数字、布尔、null、undefined;
增加事件
增加收起/展开Icon标识;
对象/数组开始标签绑定点击事件,点击后记录点击标签位置状态为收起;
遍历全部节点,将点击标签的开始位置替换为对象/数组收起节点,type='objectCollapsed'、 'arrayCollapsed',对应内容为'{...}' '[...]';
并将其子节点剔除后更新视图;
对象/数组收起节点绑定点击事件,点击后将点击标签位置更新为展开; 遍历全部节点,更新视图;
添加序号
每行增加序号显示;
完整代码
- TreeWrap.tsx
import { computed, defineComponent, reactive, watchEffect, } from 'vue';
import TreeNode from "@/components/TreeNode/TreeNode";
import { jsonFlatten } from "@/utils/utils";
export default defineComponent({
name: 'TreeWrap',
props: {
json: {
required: true,
type: Object,
default: () => ({}),
},
},
setup(props, { emit, slots }) {
const state = reactive({
useData: [] as any,
closePath: {} as any,
});
const handledJson = computed(() => {
return jsonFlatten(props.json);
});
const collapsedData = (data: any, closePath: any, path: string) => {
const newD = [];
let curClosedPath = '';
for (let ind = 0; ind < data.length; ind++) {
const ele = data[ind];
if (closePath[ele.path]) {
if (curClosedPath.includes(path)) {
// 收起节点的子节点
continue;
}
// 收起的节点
if (ele.type.includes('Start')) {
// 收起的节点,开始节点
// 记住收起节点path
curClosedPath = ele.path;
const isObject = ele.type === 'objectStart';
// 修改开始节点为收起节点
newD.push({
...ele,
content: isObject ? '{...}' : '[...]',
type: isObject ? 'objectCollapsed' : 'arrayCollapsed',
isOpen: false,
});
} else if (ele.type.includes('End')) {
// 收起的节点,结束节点
// 清除收起节点path
curClosedPath = '';
}
} else {
// 展开的节点
if (curClosedPath) {
if (ele.path.includes(curClosedPath)) {
// 处于收起节点范围内,剔除
} else {
// 不处于收起节点范围内,保留
newD.push({
...ele,
});
}
} else {
// 不处于收起节点范围内,保留
newD.push({
...ele,
});
}
}
}
return newD;
};
const iconClick = (isOpen: any, path: any) => {
state.closePath[path] = isOpen;
state.useData = collapsedData(handledJson.value, state.closePath, path);
}
watchEffect(() => {
if (props.json) {
state.useData = handledJson.value;
}
});
return () => {
return (
<div>
{
state.useData.map((item: any, index: number) => (
<TreeNode
key={`${index}`}
node={item}
indNumber={index + 1}
onIconClick={iconClick}
/>
))
}
</div>
)
};
},
});
- TreeNode.tsx
import { computed, defineComponent, reactive, } from 'vue';
import { getType } from "@/utils/utils";
import './styles.less';
export default defineComponent({
name: 'TreeNode',
props: {
node: {
required: true,
type: Object,
default: () => ({}),
},
indNumber: {
type: Number,
default: 0,
},
onIconClick: {
type: Function,
},
},
setup(props, { emit, slots }) {
const contentType = computed(() => {
return getType(props.node.content);
});
const handleIconClick = () => {
emit('iconClick', props.node.isOpen, props.node.path);
};
return () => {
const {
node,
indNumber,
} = props;
return (
<div class="tree-node-wrap">
<span class="node-index">{ indNumber }</span>
{
node.key && (
<span class="node-key">
{
Array.from(Array(node.level)).map((item: any, index: number) => (
<span
key={`indent${node.level}${index}`} class="indent-item"
/>
))
}
<span class="icon-wrap" onClick={handleIconClick}>
{
(node.type.includes('Start') || node.type.includes('Collapsed')) && node.isOpen && (
<svg
viewBox="0 0 1792 1792"
focusable="false"
data-icon="icon-minus"
width="1em"
height="1em"
fill="currentColor"
aria-hidden="true"
>
<path d="M1344 800v64q0 14-9 23t-23 9h-832q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h832q14 0 23 9t9 23zm128 448v-832q0-66-47-113t-113-47h-832q-66 0-113 47t-47 113v832q0 66 47 113t113 47h832q66 0 113-47t47-113zm128-832v832q0 119-84.5 203.5t-203.5 84.5h-832q-119 0-203.5-84.5t-84.5-203.5v-832q0-119 84.5-203.5t203.5-84.5h832q119 0 203.5 84.5t84.5 203.5z"></path>
</svg>
)
}
{
(node.type.includes('Start') || node.type.includes('Collapsed')) && !node.isOpen && (
<svg
viewBox="0 0 1792 1792"
focusable="false"
data-icon="icon-plus"
width="1em"
height="1em"
fill="currentColor"
aria-hidden="true"
>
<path d="M1344 800v64q0 14-9 23t-23 9h-352v352q0 14-9 23t-23 9h-64q-14 0-23-9t-9-23v-352h-352q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h352v-352q0-14 9-23t23-9h64q14 0 23 9t9 23v352h352q14 0 23 9t9 23zm128 448v-832q0-66-47-113t-113-47h-832q-66 0-113 47t-47 113v832q0 66 47 113t113 47h832q66 0 113-47t47-113zm128-832v832q0 119-84.5 203.5t-203.5 84.5h-832q-119 0-203.5-84.5t-84.5-203.5v-832q0-119 84.5-203.5t203.5-84.5h832q119 0 203.5 84.5t84.5 203.5z"></path>
</svg>
)
}
</span>
{ `"${node.key}": ` }
</span>
)
}
{
!node.key && node.type !== 'content' && (
<span>
{
Array.from(Array(node.level)).map((iten: any, index: number) => (
<span key={`indent${node.level}${index}`} class="indent-item"></span>
))
}
</span>
)
}
<span class="icon-wrap" onClick={handleIconClick}>
{
!node.key && (node.type.includes('Start') || node.type.includes('Collapsed')) && node.isOpen && (
<svg
viewBox="0 0 1792 1792"
focusable="false"
data-icon="icon-minus"
width="1em"
height="1em"
fill="currentColor"
aria-hidden="true"
>
<path d="M1344 800v64q0 14-9 23t-23 9h-832q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h832q14 0 23 9t9 23zm128 448v-832q0-66-47-113t-113-47h-832q-66 0-113 47t-47 113v832q0 66 47 113t113 47h832q66 0 113-47t47-113zm128-832v832q0 119-84.5 203.5t-203.5 84.5h-832q-119 0-203.5-84.5t-84.5-203.5v-832q0-119 84.5-203.5t203.5-84.5h832q119 0 203.5 84.5t84.5 203.5z"></path>
</svg>
)
}
{
!node.key && (node.type.includes('Start') || node.type.includes('Collapsed')) && !node.isOpen && (
<svg
viewBox="0 0 1792 1792"
focusable="false"
data-icon="icon-plus"
width="1em"
height="1em"
fill="currentColor"
aria-hidden="true"
>
<path d="M1344 800v64q0 14-9 23t-23 9h-352v352q0 14-9 23t-23 9h-64q-14 0-23-9t-9-23v-352h-352q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h352v-352q0-14 9-23t23-9h64q14 0 23 9t9 23v352h352q14 0 23 9t9 23zm128 448v-832q0-66-47-113t-113-47h-832q-66 0-113 47t-47 113v832q0 66 47 113t113 47h832q66 0 113-47t47-113zm128-832v832q0 119-84.5 203.5t-203.5 84.5h-832q-119 0-203.5-84.5t-84.5-203.5v-832q0-119 84.5-203.5t203.5-84.5h832q119 0 203.5 84.5t84.5 203.5z"></path>
</svg>
)
}
</span>
<span
class={[
node.type === 'content' && 'node-content',
contentType.value
]}
>
{ `${(node.type === 'content' && contentType.value === 'string') ? '"' : ''}${node.content}${(node.type === 'content' && contentType.value === 'string') ? '"' : ''}` }
{node.showComma && <span>,</span>}
{node.collapsed && (
<span class="node-collapsed">{ node.content }</span>
)}
</span>
</div>
);
}
},
});
此实现方式处理虚线和对其方式不好处理,所以修改为第二版。