G Json View 【二】

54 阅读2分钟

0-1实现一个G Json View组件。

  • 第一版

扁平化处理后渲染 G Json View 【一】 - 掘金 (juejin.cn)

  • 第二版

递归渲染。

第二版

递归渲染。

Vue3 JSX 写法递归实现。

主要实现思路

根据传入的json数据,判断数据类型。

  • 数组

    • 渲染数组的开始
    • 递归渲染数组内部结构
    • 渲染数组的结束
  • 对象

    • 渲染对象的开始
    • 递归渲染对象内部结构
    • 渲染对象的结束
  • 内容

    • 渲染内容

工具方法

用到的工具方法。

export function getType(value: any) {
  return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
}

export function isObject(value: any) {
  return getType(value) === 'object';
}

export function isArray(value: any) {
  return getType(value) === 'array';
}

主要代码

import { computed, defineComponent, reactive, watchEffect, } from 'vue';
import { isArray, isObject, getType } from "@/utils/utils";
import "./styles.less";

export default defineComponent({
  name: 'TreeWrap',

  props: {
    json: {
      required: true,
      type: Object,
      default: () => ({}),
    },
  },

  setup(props, { emit, slots }) {
    const state = reactive({
    });

    const renderItem = ({
      key, 
      data, 
      level,
      showComma,
    }: any) => {
      return (
        <div 
          class="item-wrap"
          style={{
            marginLeft: `${level}em`,
          }}
        >
          {key && <span class="node-key">{`${key}:${' '}${' '}`}</span>}
          <span
            class={[
              'node-content',
              ['{', '}', '[', ']'].includes(data) ? '' : getType(data)
            ]}
          >
            {`${data}`}
          </span>
          {showComma && <span>{','}</span>}
        </div>
      );
    }

    const renderContent = ({
      // 键值
      key = null,
      // json数据
      data,
      // 层级
      level = 0,
      // 是否显示逗号
      showComma = false,
    }: any) => {
      if (isArray(data)) {
        return (
          <div 
            class="content-wrap"
            style={{
              // marginLeft: `${level}em`,
            }}
          >
            {
              renderItem({
                key,
                data: '[',
                level,
                showComma: false
              })
            }
            {
              data.map((item: any, index: number) => {
                return renderContent({
                  key,
                  data: item, 
                  level: level + 1,
                  showComma: index !== data.length - 1
                });
              })
            }
            {
              renderItem({
                key: null,
                data: ']',
                level,
                showComma,
              })
            }
          </div>
        )
      }
      if (isObject(data)) {
        return (
          <div 
            class="content-wrap"
            style={{
              // marginLeft: `${level}em`,
            }}
          >
            {
              renderItem({
                key,
                data: '{',
                level,
                showComma: false
              })
            }
            {
              Object.keys(data).map((key: any, index: number) => {
                return renderContent({
                  key,
                  data: data[key], 
                  level: level + 1,
                  showComma: index !== Object.keys(data).length - 1
                });
              })
            }
            {
              renderItem({
                key: null,
                data: '}',
                level,
                showComma,
              })
            }
          </div>
        );
      }
      return renderItem({
        key,
        data,
        level,
        showComma,
      });
    }
    
    return () => {
      return (
        <div class='tree-node-wrap'>
          {
            renderContent({data: props.json})
          }
        </div>
      )
    };
  },
});

.tree-node-wrap {
  .node-index {
    display: inline-block;
    width: 1em;
    color: rgb(88, 110, 117);
  }
  .icon-wrap {
    cursor: pointer;
    display: inline-block;
    svg {
      vertical-align: middle; 
      color: rgb(88, 110, 117); 
      height: 1em; 
      width: 1em;
    }
  }
  .node-key {
    color: #8c6325;
  }
  .node-content {
    color: #000;
  }
  .node-content.string {
    color: #57b73b;
  }
  .node-content.number {
    color: #2d8cf0;
  }
  .node-content.boolean {
    color: #1d8ce0;
  }
  .node-content.null {
    color: #D55FDE;
  }
  .node-content.undefined {
    color: #D55FDE;
  }
}

以上基本就可以实现一个json的渲染展示了,后续主要就是一些优化和细节的处理。

优化和细节的处理

以下修改均在 `src/components/TreeWrap/TreeWrap.tsx``

  • 层级样式优化(缩进和边框)
  • 是否显示逗号
  • 添加Icon

增加点击事件

  • 绑定事件
  • 点击后记录点击的位置,将点击范围内渲染替换为折叠样式
  • 折叠时展示条数

增加配置项

  • 深度,大于等于该深度的节点将被折叠。
  props: {
    ...
    deep: {
      type: Number,
      default: 4
    },
  }

  setup(props, { emit, slots }) {
    const pathOutDeep = (path: string) => {
      if (props.deep) {
        return path.split('[').length >= props.deep
      }
      return false
    }

    const renderContent = ({
      ...
    }: any) => {
      const isClosed = state.closedPath[path] === undefined ? pathOutDeep(path) : state.closedPath[path]
      ...
    }
  }
  • 在数据折叠的时候展示长度
  props: {
    ...
    showLength: {
      type: Boolean,
      default: true
    },
  }

  setup(props, { emit, slots }) {
    const renderItem = ({
      ...
    }: NodeDataType) => {
      return (
        <div
          class={['item-wrap', level > 0 && 'need-indent', canClick && 'item-wrap-click']}
          onClick={() => canClick && handleIconClick(isClosed, path)}
        >
          ...
          {props.showLength && !!itemsLen && <span class="items-length">{`${itemsLen} items`}</span>}
          ...
        </div>
      )
    }
  }
  • 展示标识线
  props: {
    ...
    showLine: {
      type: Boolean,
      default: true
    },
  }

  setup(props, { emit, slots }) {
    const renderContent = ({
      ...
    }: any) => {
      ...
      if (isArray(data)) {
        ...
        return (
          <div class={['content-wrap', props.showLine && level > 0 && 'need-border', level > 1 && 'need-indent']}>
            ...
          </div>
        )
      }
      if (isObject(data)) {
        ...
        return (
          <div class={['content-wrap', props.showLine && level > 0 && 'need-border', level > 1 && 'need-indent']}>
            ...
          </div>
        )
      }
      return (
        <div class={['content-wrap', props.showLine && level > 0 && 'need-border', level > 1 && 'need-indent']}>
          ...
        </div>
      )
    }
  }
  • 展示图标
  props: {
    ...
    showIcon: {
      type: Boolean,
      default: true
    },
  }

  setup(props, { emit, slots }) {
    const renderContent = ({
      ...
    }: any) => {
      ...
      if (isArray(data)) {
        ...
        return (
          <div class={['content-wrap', props.showLine && level > 0 && 'need-border', level > 1 && 'need-indent']}>
            {props.showIcon && renderIcon(isClosed, path)}
            ...
          </div>
        )
      }
      if (isObject(data)) {
        ...
        return (
          <div class={['content-wrap', props.showLine && level > 0 && 'need-border', level > 1 && 'need-indent']}>
            {props.showIcon && renderIcon(isClosed, path)}
            ...
          </div>
        )
      }
    }
  }
  • 展示 key 名的双引号
  props: {
    ...
    showDoubleQuotes: {
      type: Boolean,
      default: true
    },
  }

  setup(props, { emit, slots }) {
    const defaultKey = (key: string) => (
      <span class={['node-key']}>
        {`${props.showDoubleQuotes && '"'}${key}${props.showDoubleQuotes && '"'}:${' '}${' '}`}
      </span>
    )

    const defaultValue = (value: any) => {    
      if (value === null) {
        value = 'null';
      } else if (value === undefined) {
        value = 'undefined';
      }
      return getType(value) === 'string' ? `${isBracket(value) ? '' : props.showDoubleQuotes && '"'}${value}${isBracket(value) ? '' : props.showDoubleQuotes && '"'}` : value + '';
    }
  }
  • 定义最顶层数据路径
  props: {
    ...
    rootPath: {
      type: String,
      default: 'root'
    },
  }

  setup(props, { emit, slots }) {
    const renderContent = ({
      ...
      // 路径
      path = props.rootPath || 'root'
    }: any) => {
      ...
    }
  }
  • 支持点击括号或文字折叠
  props: {
    ...
    collapsedOnClickBrackets: {
      type: Boolean,
      default: true
    },
  }

  setup(props, { emit, slots }) {
    const renderItem = ({
      ...
    }: NodeDataType) => {
      return (
        <div
          class={['item-wrap', level > 0 && 'need-indent', props.collapsedOnClickBrackets && canClick && 'item-wrap-click']}
          onClick={() => props.collapsedOnClickBrackets && canClick && handleIconClick(isClosed, path)}
        >
          ...
        </div>
      )
    }
  }
  • 自定义渲染节点键
  props: {
    ...
    renderNodeKey: {
      type: Function as PropType<
        (opt: { node: NodeDataType; defaultKey: string | JSX.Element }) => unknown
      >
    },
  },
  setup(props, { emit, slots }) {
    ...

    const defaultKey = (key: string) => (
      <span class={['node-key']}>
        {`${props.showDoubleQuotes && '"'}${key}${props.showDoubleQuotes && '"'}:${' '}${' '}`}
      </span>
    )

    const renderKey = ({
      key,
      data,
      level,
      showComma,
      path,
      itemsLen,
      isClosed,
      canClick
    }: NodeDataType) => {
      if (props.renderNodeKey) {
        return props.renderNodeKey({
          node: {
            key,
            data,
            level,
            showComma,
            path,
            itemsLen,
            isClosed,
            canClick
          }, 
          defaultKey: defaultKey(key)
        })
      }
      return defaultKey(key);
    }
  }
  • 自定义渲染节点值
  props: {
    ...
    renderNodeValue: {
      type: Function as PropType<
        (opt: { node: NodeDataType; defaultValue: string | JSX.Element }) => unknown
      >
    }
  },
  setup(props, { emit, slots }) {
    ...

    const defaultValue = (value: any) => {    
      if (value === null) {
        value = 'null';
      } else if (value === undefined) {
        value = 'undefined';
      }
      return getType(value) === 'string' ? `${isBracket(value) ? '' : props.showDoubleQuotes && '"'}${value}${isBracket(value) ? '' : props.showDoubleQuotes && '"'}` : value + '';
    }

    const renderValue = ({
      key,
      data,
      level,
      showComma,
      path,
      itemsLen,
      isClosed,
      canClick
    }: NodeDataType) => {
      if (props.renderNodeValue) {
        return props.renderNodeValue({
          node: {
            key,
            data,
            level,
            showComma,
            path,
            itemsLen,
            isClosed,
            canClick
          }, 
          defaultValue: defaultValue(data)
        })
      }
      return defaultValue(data);
    }
  }
  • 点击节点时触发
  props: {
    ...
    nodeClick: {
      type: Function as PropType<
        (opt: { isClosed: Boolean, path: String }) => unknown
      >
    }
  },
  setup(props, { emit, slots }) {
    ...

    const handleIconClick = (isClosed: boolean, path: string) => {
      props.nodeClick && props.nodeClick({isClosed, path});
      ...
    }
  }

此方式不好加序号。

完整代码

Github:G Json View