G Json View 【一】

130 阅读3分钟

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数据按标记渲染;

优化

增加层级标识,分层级缩进; 之前版本逗号显示有问题,修改; 纯文本最后一个元素和整体最后一个元素不加逗号; 增加双引号; 纯文本样式:字符串、数字、布尔、nullundefined

增加事件

增加收起/展开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>
      );
    }
  },
});

此实现方式处理虚线和对其方式不好处理,所以修改为第二版。