手把手教你实现一个JSON解析器

1 开始

当我们拿到后端返回的一长串JSON数据后,面对这一长串的字符串非常头疼,通常我们会去一些工具网站找到一些JSON解析的工具来愉快的查看,在浏览了那么多JSON解析的网站后,我发现除了JSON解析外还有那么多关于JSON字符串的小功能,格式化、压缩、修复、下载…,不仅感叹平日开发里一个小小的JSON也能有这么多花样,于是产生了自己来实现一个JSON解析的效果,经过一番花里胡哨的操作后,先看我们最终的实现效果:
在这里插入图片描述

在这篇文章中,我们将从0到1实现上面的全部功能,但在开始前,我们有必要再来复习下在js这门语言中,关于JSON解析和反转的字符串的两个重要API:

  • JSON.parse() 方法用来解析 JSON 字符串,构造由字符串描述的 JavaScript 值或对象。提供可选的 reviver 函数用以在返回之前对所得到的对象执行变换 (操作)。

  • JSON.stringify() 方法将一个 JavaScript 对象或值转换为 JSON 字符串,如果指定了一个 replacer 函数,则可以选择性地替换值,或者指定的 replacer 是数组,则可选择性地仅包含数组指定的属性。

平时开发时我们用到这两个方法时更多的是只关注第一个参数,忽略了其它参数,这里我们来列举下JSON.stringify(value[, replacer [, space]])的其它参数。JSON.stringify() - JavaScript | MDN (mozilla.org)

  • value
    将要序列化成 一个 JSON 字符串的值。
  • replacer 可选
    如果该参数是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理;如果该参数是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中;如果该参数为 null 或者未提供,则对象所有的属性都会被序列化。
  • space 可选
    指定缩进用的空白字符串,用于美化输出(pretty-print);如果参数是个数字,它代表有多少的空格;上限为 10。该值若小于 1,则意味着没有空格;如果该参数为字符串(当字符串长度超过 10 个字母,取其前 10 个字母),该字符串将被作为空格;如果该参数没有提供(或者为 null),将没有空格。

这边我们重点关注第三个参数,简单来说就是规定我们调用这个方法进行JSON序列化时,属性前补几个空格,这也是我们实现json格式化的重要方法,下面我们来举个简单的例子:

let info = {
  name: "M",
  age: 18,
};
console.log("不传入第三个参数 \n", JSON.stringify(info));
console.log("传入第三个参数 \n", JSON.stringify(info, null, 2));

在这里插入图片描述

2.实现JSON解析功能

本次实现我们主要来实现关于JSON解析部分的代码,布局请自行编写,另外选择框架为React,没有React基础也可安心食用,本次功能实现更多的是使用原生js,了解了思路,使用其它技术来实现也能手拿把捏。先来总结下实现思路,经过观察不难发现,左侧的文本域中是纯字符串,而右侧解析过的结果有文本样式、可折叠效果、行号等,想要实现这种效果我们要做的就是,先将文本通过JSON.parse()转换为js对象,进而我们通过遍历对象生成右侧dom结构,最后通过innerHTML来让浏览器渲染这些dom元素。

2.1 基本布局

明确了思路后我们来实现基本布局,由于josn字符串太长,我们的代码就不粘了,大家可以自行准备,下面我们贴上基本布局的代码。

import styles from "./jsonParse.module.less";
import { useState } from "react";
import { Input } from "antd";

const { TextArea } = Input;

const JsonParse = () => {
  // 输入框字符串
  let [str, setStr] = useState(``);
  // html字符串(也就是最终渲染的字符串)
  const [html, setHtml] = useState("");
  // 生成html结构
  const generateHtmlStr = (str: string) => {
    return str;
  };

  return (
    <>
      <div className={styles.wrap}>
        <div className={styles.editor}>
          <TextArea
            value={str}
            style={{ height: "80vh", fontSize: "16px" }}
            onChange={e => {
              setStr(e.target.value);
              setHtml(generateHtmlStr(e.target.value));
            }}
          />
        </div>
        <div className={styles.preview}>
          <div dangerouslySetInnerHTML={{ __html: html }}></div>
        </div>
      </div>
    </>
  );
};

export default JsonParse;

2.2 实现转换函数

完成基本布局后我们第一步就是要来实现转换函数,也就是上面写的generateHtmlStr方法,它接收一个字符串,也就是我们文本域中输入的字符串,我们要做的就是将字符串转换成html格式的字符串,下面针对它的转换前和转换后,来演示下简单的示例。

let str = `
{
  "name": "M",
  "age": 18,
  "hobbies":["music", "reading", "swimming"]
}
`

// ⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇ 调用方法 ⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇ 调用方法 ⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇
let html = generateHtmlStr(str)
// ⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇ 转换后 ⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇ 转换后 ⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇

<div class="wrap">
  <span class="bound-el">{</span/>
  <div class=json-item>
    <span class=prefix-key>"name": </span>
    <span class=str-value>"M"</span>  
  </div>
  // 这里用json-item这个类名来表示内容是非对象 
  <div class=json-item>
    <span class=prefix-key>"age": </span>
    <span class=str-value>18</span>  
  </div>
  <div class=json-item>
    <span class=prefix-key>"hobbies": </span>
    <span class="bound-el">[</span/>
    // 这里用json-object这个类型来表示这是一个对象 
    <div class=json-object id="json-object">
       <div class=json-item>
         <span class=str-value>"music"</span>
       </div>
       <div class=json-item>
         <span class=str-value>"reading"</span>
       </div>
       <div class=json-item>
         <span class=str-value>"swimming"</span>
       </div>
    </div>
    <span class="bound-el">]</span/>
  </div>
  <span class="bound-el">}</span>                           
</div>

看到这儿是不是就觉得非常明了了,我们只需要把div元素对用的标签加上margin-left,键名和键值对应的元素加上颜色,你就发现,欸,好像直接这样就成了!于是怀揣着激动的心情我们将这个转换函数实现下,代码注释写的很全,供大家编写时参考或优化。

generateHtmlStr方法代码:

  const generateHtmlStr = (str: string) => {
    // 定义左右括号
    const obj_left = `<span class="bound-el">{</span/>`;
    const obj_right = `<span class="bound-el">}</span/>`;
    const arr_left = `<span class="bound-el">[</span/>`;
    const arr_right = `<span class="bound-el">]</span/>`;

    // JSON.parse可能解析json字符串失败,所以需要try catch
    try {
      const mainFn = (str: string) => {
        // 解析出来的json对象
        const json = JSON.parse(str);
        let result = `<div class=${styles["json-object"]} id="json-object">`;

        // 遍历json对象
        for (let key in json) {
          // item表示json对象中的每一个值
          let item = json[key];
          // 如果父节点是数组的话,数组内的属性不要属性名,也就是不要key
          const prefixKey = isUtils.isArray(json)
            ? ""
            : `<span class=${styles["prefix-key"]}>"${key}": </span>`;
          // 如果要处理的元素是数组,这里要注意,数组里面可以放数组或者对象,所以递归调用
          if (item instanceof Array) {
            let value = `${arr_left} ${item.length == 0 ? "" : mainFn(JSON.stringify(item))} ${arr_right}`;
            // 组装
            result += `<div class=${styles["json-item"]}>${prefixKey}${value}</div>`;
          }
          // 如果要处理的元素是对象,这里要注意,对象里面的键值可以是数组或者对象,所以递归调用
          else if (item instanceof Object) {
            let value = `${obj_left} ${Object.keys(item).length == 0 ? "" : mainFn(JSON.stringify(item))} ${obj_right}`;
            // 组装
            result += `<div class=${styles["json-item"]}>${prefixKey}${value}</div>`;
          }
          // 其它元素处理
          else {
            // 字符串类型外面要加双引号
            if (typeof item === "string") {
              // 防止字符串中有html标签,所以需要转义
              item = `<span class=${styles["str-value"]}>"${item.replace(/[\\<>&"]/g, function (c) {
                return { "<": "&lt;", ">": "&gt;", "&": "&amp;", '"': "&quot;" }[c]!;
              })}"</span>`;
            } else if (typeof item === "number") {
              item = `<span class=${styles["number-value"]}>${item}</span>`;
            } else if (typeof item === "boolean") {
              item = `<span class=${styles["bool-value"]}>${item}</span>`;
            } else if (item === null) {
              item = `<span class=${styles["null-value"]}>${item}</span>`;
            }
            // 组装
            result += `<div class=${styles["json-item"]}>${prefixKey}${item}</div>`;
          }
        }
        return result + `</div>`;
      };

      // 根据最外层是对象还是数组,来决定左右括号
      let result = "";
      if (isUtils.isArray(JSON.parse(str))) {
        result = `<div class="wrap">${arr_left} ${mainFn(str)} ${arr_right}</div>`;
      } else {
        result = `<div class="wrap">${obj_left} ${mainFn(str)} ${obj_right}</div>`;
      }
      return result;
    } catch (err) {
      // 如果解析失败,则返回错误信息
      return `<div class="wrap" style="color:red">
          <div>请输入正确的json格式:</div>
         ${err}
       </div>`;
    }
  };

在这里插入图片描述

经过掉落一根头发的代价,我们就能得到了上面看到的效果,瞬间又觉得自己强的可怕。至此,我们的主要功能也就完成了,能够根据愉快的预览JSON数据了。接下来,我们来一次实现下其它拓展功能,let‘s go!

2.3 显示竖线

接下来我们我实现对象或数组竖线的显示,根据2.2节内容的分析及generateHtmlStr方法的实现,我们可以知道,对象和数组都是类名为json-obejct的div,所以我们直接为这个类名添加边框,边框颜色红、橙、黄、绿…任你选择,下面为了更清晰的展示,我们直接显示盒子的全部边框,如果需要实现我们的最终效果,只需要保留左边框即可
在这里插入图片描述

2.4 展开&收起

要实现展开和收起的功能,首要要明白,展开和收起按钮其实就是一个dom元素。那么,什么数据才需要展开和收起呢?需要展开和收起的元素是对象或者数组。那么,我们怎么定位到对象和数组并添加展开按钮呢?我们只需要在处理对象或者数组的时候,在最外层的div内多拼接一个展开按钮,并设置定位,定位到div的最左侧。最后添加属性data-expanding = “true”,表示这个对象是展开状态,方便我们后来收起时判断。

const generateHtmlStr = (str:string)=>{
  // 定义展开按钮
  const expandDiv = `<div class=${styles["expand-div"]} id="expand-div" data-expanding="true">▼</div>`;
  //....省略重复代码
  const mainFn = (str: string) => {
    const json = JSON.parse(str);
    let result = `<div class=${styles["json-object"]} id="json-object">${expandDiv}`;
     //....省略重复代码
  };
}

在这里插入图片描述

这个时候,我们的展开按钮就能正常显示了,但是目前都是静态的,点击并不会有回馈。在进行交互前,我们还需要做一件事,观察我们的最终效果可以看出来,当我们的对象或数组收起的时候,后面会有一个数字,表示当前收起的对象里面有多少属性。首先我们要知道这个”数字“也是一个dom元素,所以又回到上面的问题了,我们只需要再处理对象和数组的时候在外层div内拼接一个dom元素,dom元素的内容就是属性的数量,并且默认设置为隐藏状态(display:none)。我的处理是,定义一个方法getCountEl,为了方便演示,我将默认的显示状态设置为显示,如果需要实现最终效果,设置默认状态为隐藏

const generateHtmlStr = (str:string)=>{
  // 定义展开按钮
  const expandDiv = `<div class=${styles["expand-div"]} id="expand-div" data-expanding="true">▼</div>`;
  // 统计子元素数量方法
  let getCountEl = (count: number, type: string) => {
    let content = ``;
    if (type == "array") {
      content = `[ ${count} ]`;
    } else if (type == "object") {
      content = `{ ${count} }`;
    }
    return count  ? `<div class=count-el style="display: block;color:#00f;font-size:16px;text-shadow:0 0   10px #00f;padding-left:2px;">${content}</div>`: "";
  };
  
  //....省略重复代码
  const mainFn = (str: string) => {
    const json = JSON.parse(str);
    let result = `<div class=${styles["json-object"]} id="json-object">${expandDiv}`;
     //....省略重复代码
    
     for (let key in json) {
       let item = json[key];
        //....省略重复代码
        // 数组 
        if (item instanceof Array) {
           // 在json-object中拼接统计子元素数量的dom
           let value = `${getCountEl(item.length, "array")} ${arr_left} ${item.length == 0 ? "" : mainFn(JSON.stringify(item))} ${arr_right}`;
           result += `<div class=${styles["json-item"]}>${prefixKey}${value}</div>`;
          }
          // 对象
          else if (item instanceof Object) {
            // 同理
            let value = `${getCountEl(Object.keys(item).length, "object")}${obj_left} ${Object.keys(item).length == 0 ? "" : mainFn(JSON.stringify(item))} ${obj_right}`;
            result += `<div class=${styles["json-item"]}>${prefixKey}${value}</div>`;
          }
        //....省略重复代码
        return result + `</div>`;
      };
  };
}

在这里插入图片描述
最后我们只需要完成交互部分的代码,展开和收起功能就大功告成了!接下来,我们来实现这部分的交互,首先整理思路:获取到全部的展开或收起按钮并为其绑定鼠标点击事件,那么点击的时候我们都需要做哪些操作呢?首先通过data-expanding属性判断按钮的状态,这里我们距离展开按钮来说,当我们点击后,我们想要的是收起这个对象。那么我们可以通过按钮这个dom元素获取到他的兄弟节点,我们只需要将兄弟节点隐藏,并且将统计数字和边界括号的dom元素显示,即可实现收起的效果。收起效果反之,流程清楚后就可以编写交互代码了,这边我贴上我的代码作为参考:

// 等待dom渲染完成
useEffect(() => {
  // 获取全部的展开或收起按钮
  let allExpandBtn = document.querySelectorAll("#expand-div");
  if (allExpandBtn.length) {
    // 遍历为全部的展开或收起按钮添加点击事件
    allExpandBtn.forEach(expandBtn => {
      expandBtn.addEventListener("click", _e => {
        // 当前点击的按钮
        let currentExpandBtn = _e.target as HTMLElement;
        // 当前点击按钮的父节点
        let parentEl = currentExpandBtn.parentElement as HTMLElement;
        // 当前按钮的全部兄弟节点
        let brotherNodes = Array.from(
          parentEl.children as HTMLCollectionOf<HTMLDivElement>,
        ).filter(div => div.getAttribute("id") != "expand-div");
        // 当前点击按钮的count-el(统计属性数量的元素)
        let countEl = Array.from(
          parentEl.parentElement?.children as HTMLCollectionOf<HTMLDivElement>,
        ).filter(div => div.getAttribute("class")?.includes("count-el"))[0];
        // 获取边界符(左右括号或左右中括号)
        const boundElList = Array.from(
          parentEl.parentElement?.children as HTMLCollectionOf<HTMLDivElement>,
        ).filter(div => div.getAttribute("class")?.includes("bound-el"));
        // 当前节点是否展开
        let { expanding } = currentExpandBtn.dataset;
        // 如果状态是展开,则收起
        if (expanding) {
          // 将状态改为收起中
          currentExpandBtn.dataset.expanding = "";
          // 收起按钮改为展开
          currentExpandBtn.innerHTML = "▶";
          countEl.style.display = "inline-block";
          // 隐藏全部兄弟节点
          brotherNodes.forEach(div => (div.style.display = "none"));
          // 隐藏边界符({ } 或 [ ])
          boundElList.forEach(div => (div.style.display = "none"));
        } else {
          // 将状态改为展开中
          currentExpandBtn.dataset.expanding = "true";
          // 展开按钮改为收起
          currentExpandBtn.innerHTML = "▼";
          // 隐藏count-el
          countEl.style.display = "none";
          // 显示全部兄弟节点
          brotherNodes.forEach(div => (div.style.display = ""));
          // 显示边界符
          boundElList.forEach(div => (div.style.display = ""));
        }
      });
    });
  }
}, [html]);

在这里插入图片描述

2.5 代码行数

代码行数的展示其实就是一个div里面有很多小的div,使用flex布局让小的div纵向布局,使用绝对定位定位到父元素的最左侧。那么如何来确定JSON代码有多少行呢?很简单的办法,保证容器内的元素高度都一样,然后通过获取容器的高度除以子元素的高度就得到代码行数了注意,每次点击展开或收起都需要重新计算

// 行数
const [rowNum, setRowNum] = useState(0);
// ...

// 计算行数,行高为24px
let previewHeight = document.querySelector(".wrap")?.scrollHeight;
if (previewHeight) setRowNum(Math.ceil(previewHeight / 24));
// ...

// html部分
<div className={styles["line-numbers-wrapper"]}>
{Array(rowNum)
  .fill(0)
  .map((_item, index) => {
    return (
      <div className={styles["line-number"]} key={index}>
        {index + 1}
      </div>
    );
  })}
</div>

在这里插入图片描述

至此,完结散花,我们的JSON解析器功能就完成了。

3.其它功能的实现

这里我们要实现的则是一些次要功能,也就是文本域最顶部的一些按钮功能,下面的小节,我们来一起过一遍
在这里插入图片描述

3.1 JSON格式化和压缩

在第一张就有所介绍,格式化和压缩主要借助的就是JSON.stringify()方法,通过设置第三个参数来实现JSON的格式化,相反如果不传入第三个参数就是压缩,需要注意使用cry catch捕获错误,这里的JSON.parse可能失败。

 // 格式化json
  const handleFormat = () => {
    try {
      let jsonObject = JSON.parse(str);
      setStr(JSON.stringify(jsonObject, null, 4));
    } catch (err) {
      message.error("请输入正确的json格式");
    }
  };
  // 压缩json
  const handleMinify = () => {
    try {
      let jsonObject = JSON.parse(str);
      setStr(JSON.stringify(jsonObject));
    } catch (err) {
      message.error("请输入正确的json格式");
    }
  };

3.2 JSON修复

主要思路就是利用正则匹配错误的字符,然后进行替换修复,博主的正则水平堪忧,这里的代码仅作参考(只能进行简单的修复),实际实现可以完善。

  // json修复
  const handleRepair = () => {
    // 去掉多余的,
    str = str.replace(/,\s*([\]}])/g, "$1");
    // 匹配没有引号的键并补上引号
    str = str.replace(/([{,]\s*)(\w+)\s*:/g, '$1"$2":');
    // 匹配没有引号的字符串并补上引号
    str = str.replace(/:\s*"\s*,/g, ': "",');
    // 将单引号替换为双引号
    str = str.replace(/'/g, '"');
    // 5. 修复未闭合的对象或数组
    if (str.trim().startsWith("{") && !str.trim().endsWith("}")) str += "}";
    if (str.trim().startsWith("[") && !str.trim().endsWith("]")) str += "]";
    setStr(str);
    setHtml(generateHtmlStr(str));
  };

3.3 复制

复制主要调用navigator.clipboard的相关方法Clipboard - Web API | MDN (mozilla.org),注意,这个api的方法只能应用于https和localhost,另外存在很小的兼容性问题,具体可以参考MDN。

  // 复制json
  const handleCopy = () => {
    // ClipboardUtil 是我做的封装,文章最后会有这个对象实现的代码
    ClipboardUtil.writeText(str);
    message.success("复制成功^_^");
  };

3.4 下载

下载的思路是通过将json字符串转为blob,然后通过a标签的方式下载。

  // 下载json
  const handleDownload = () => {
    // FileUtils 是我做的封装,文章最后会有这个对象实现的代码
    const blob = new Blob([str], { type: "text/plain;charset=utf-8" });
    FileUtils.downloadFileByUrl(URL.createObjectURL(blob), Date.now() + ".json");
  };

3.5 全屏

  // 全屏
  const previewRef = useRef(null); // 这个元素是右侧负责预览的dom元素
  const handleFullScreen = () => {
    if (previewRef.current) {
      (previewRef.current as HTMLAnchorElement).requestFullscreen();
    }
  };

4. 完整代码

4.1 jsonParse.tsx 主文件

import styles from "./jsonParse.module.less";
import { useState, useEffect, useRef } from "react";
import { Button, Input, Space, message } from "antd";
import isUtils from "@/utils/is";
import ClipboardUtil from "@/utils/clipboardUtil";
import FileUtils from "@/utils/fileUtils";

const { TextArea } = Input;

const JsonParse = () => {
  // 格式化json
  const handleFormat = () => {
    try {
      let jsonObject = JSON.parse(str);
      setStr(JSON.stringify(jsonObject, null, 4));
    } catch (err) {
      message.error("请输入正确的json格式");
    }
  };
  // 压缩json
  const handleMinify = () => {
    try {
      let jsonObject = JSON.parse(str);
      setStr(JSON.stringify(jsonObject));
    } catch (err) {
      message.error("请输入正确的json格式");
    }
  };
  // json修复
  const handleRepair = () => {
    // 去掉多余的,
    str = str.replace(/,\s*([\]}])/g, "$1");
    // 匹配没有引号的键并补上引号
    str = str.replace(/([{,]\s*)(\w+)\s*:/g, '$1"$2":');
    // 匹配没有引号的字符串并补上引号
    str = str.replace(/:\s*"\s*,/g, ': "",');
    // 将单引号替换为双引号
    str = str.replace(/'/g, '"');
    // 5. 修复未闭合的对象或数组
    if (str.trim().startsWith("{") && !str.trim().endsWith("}")) str += "}";
    if (str.trim().startsWith("[") && !str.trim().endsWith("]")) str += "]";
    setStr(str);
    setHtml(generateHtmlStr(str));
  };
  // 复制json
  const handleCopy = () => {
    ClipboardUtil.writeText(str);
    message.success("复制成功^_^");
  };
  // 下载json
  const handleDownload = () => {
    const blob = new Blob([str], { type: "text/plain;charset=utf-8" });
    FileUtils.downloadFileByUrl(URL.createObjectURL(blob), Date.now() + ".json");
  };
  // 全屏
  const previewRef = useRef(null);
  const handleFullScreen = () => {
    if (previewRef.current) {
      (previewRef.current as HTMLAnchorElement).requestFullscreen();
    }
  };

  // 输入框字符串
  let [str, setStr] = useState(``);
  // html字符串(也就是最终渲染的字符串)
  const [html, setHtml] = useState("");
  // 行数
  const [rowNum, setRowNum] = useState(0);
  // 生成html结构
  const generateHtmlStr = (str: string) => {
    // 展开和合并按钮
    const expandDiv = `<div class=${styles["expand-div"]} id="expand-div" data-expanding="true">▼</div>`;
    // 如果子节点是数组或者对象,统计子元素数量,并隐藏
    let getCountEl = (count: number, type: string) => {
      let content = ``;
      if (type == "array") {
        content = `[ ${count} ]`;
      } else if (type == "object") {
        content = `{ ${count} }`;
      }
      return count
        ? `<div class=count-el style="display: none;color:#00f;font-size:16px;text-shadow:0 0 10px #00f;padding-left:2px;">${content}</div>`
        : "";
    };
    // 定义左右括号
    const obj_left = `<span class="bound-el">{</span/>`;
    const obj_right = `<span class="bound-el">}</span/>`;
    const arr_left = `<span class="bound-el">[</span/>`;
    const arr_right = `<span class="bound-el">]</span/>`;
    try {
      const mainFn = (str: string) => {
        const json = JSON.parse(str);
        let result = `<div class=${styles["json-object"]} id="json-object">${expandDiv}`;

        for (let key in json) {
          let item = json[key];
          // 如果父节点是数组的话,数组内的属性不要属性名,也就是不要key
          const prefixKey = isUtils.isArray(json)
            ? ""
            : `<span class=${styles["prefix-key"]}>"${key}": </span>`;
          // 数组
          if (item instanceof Array) {
            let value = `${getCountEl(item.length, "array")} ${arr_left} ${item.length == 0 ? "" : mainFn(JSON.stringify(item))} ${arr_right}`;
            result += `<div class=${styles["json-item"]}>${prefixKey}${value}</div>`;
          }
          // 对象
          else if (item instanceof Object) {
            let value = `${getCountEl(Object.keys(item).length, "object")}${obj_left} ${Object.keys(item).length == 0 ? "" : mainFn(JSON.stringify(item))} ${obj_right}`;
            result += `<div class=${styles["json-item"]}>${prefixKey}${value}</div>`;
          }
          // 其它
          else {
            // 字符串类型外面要加双引号
            if (typeof item === "string") {
              // 转义标签
              item = `<span class=${styles["str-value"]}>"${item.replace(/[\\<>&"]/g, function (c) {
                return { "<": "&lt;", ">": "&gt;", "&": "&amp;", '"': "&quot;" }[c]!;
              })}"</span>`;
            } else if (typeof item === "number") {
              item = `<span class=${styles["number-value"]}>${item}</span>`;
            } else if (typeof item === "boolean") {
              item = `<span class=${styles["bool-value"]}>${item}</span>`;
            } else if (item === null) {
              item = `<span class=${styles["null-value"]}>${item}</span>`;
            }
            result += `<div class=${styles["json-item"]}>${prefixKey}${item}</div>`;
          }
        }
        return result + `</div>`;
      };

      // 根据最外层是对象还是数组
      let result = "";
      if (isUtils.isArray(JSON.parse(str))) {
        result = `<div class="wrap">${getCountEl(JSON.parse(str).length, "array")} ${arr_left} ${mainFn(str)} ${arr_right}</div>`;
      } else {
        result = `<div class="wrap">${getCountEl(Object.keys(JSON.parse(str)).length, "object")} ${obj_left} ${mainFn(str)} ${obj_right}</div>`;
      }
      return result;
    } catch (err) {
      return `<div class="wrap" style="color:red">
         <div>请输入正确的json格式:</div>
        ${err}
      </div>`;
    }
  };

  useEffect(() => {
    setHtml(generateHtmlStr(str));
  }, []);

  // 等待dom渲染完成
  useEffect(() => {
    let allExpandBtn = document.querySelectorAll("#expand-div");
    if (allExpandBtn.length) {
      allExpandBtn.forEach(expandBtn => {
        expandBtn.addEventListener("click", _e => {
          // 获取当前点击的按钮
          let currentExpandBtn = _e.target as HTMLElement;
          // 获取当前按钮的父节点
          let parentEl = currentExpandBtn.parentElement as HTMLElement;
          // 获取当前按钮的全部兄弟节点
          let brotherNodes = Array.from(
            parentEl.children as HTMLCollectionOf<HTMLDivElement>,
          ).filter(div => div.getAttribute("id") != "expand-div");
          // 获取当前点击按钮的count-el
          let countEl = Array.from(
            parentEl.parentElement?.children as HTMLCollectionOf<HTMLDivElement>,
          ).filter(div => div.getAttribute("class")?.includes("count-el"))[0];
          // 获取边界符
          const boundElList = Array.from(
            parentEl.parentElement?.children as HTMLCollectionOf<HTMLDivElement>,
          ).filter(div => div.getAttribute("class")?.includes("bound-el"));

          // 当前节点是否展开
          let { expanding } = currentExpandBtn.dataset;
          // 如果状态是展开,则收起
          if (expanding) {
            // 将状态改为收起中
            currentExpandBtn.dataset.expanding = "";
            // 收起按钮改为展开
            currentExpandBtn.innerHTML = "▶";
            countEl.style.display = "inline-block";
            // 隐藏全部兄弟节点
            brotherNodes.forEach(div => (div.style.display = "none"));
            // 隐藏边界符({ } 或 [ ])
            boundElList.forEach(div => (div.style.display = "none"));
          } else {
            // 将状态改为展开中
            currentExpandBtn.dataset.expanding = "true";
            // 展开按钮改为收起
            currentExpandBtn.innerHTML = "▼";
            // 隐藏count-el
            countEl.style.display = "none";
            // 显示全部兄弟节点
            brotherNodes.forEach(div => (div.style.display = ""));
            // 显示边界符
            boundElList.forEach(div => (div.style.display = ""));
          }
          // 如果高度发生变化,重新计算行数
          let previewHeight = document.querySelector(".wrap")?.scrollHeight;
          setRowNum(Math.ceil(previewHeight! / 24));
        });
      });
    }
    // 计算行数,行高为24px
    let previewHeight = document.querySelector(".wrap")?.scrollHeight;
    if (previewHeight) setRowNum(Math.ceil(previewHeight / 24));
  }, [html]);

  return (
    <>
      <div className={styles.wrap}>
        <div className={styles.editor}>
          <div className={styles.action}>
            <Space>
              <Button type="primary" ghost onClick={handleFormat}>
                格式化
              </Button>
              <Button type="primary" ghost onClick={handleMinify}>
                压缩
              </Button>
              <Button type="primary" ghost onClick={handleRepair}>
                修复
              </Button>
              <Button type="primary" ghost onClick={handleCopy}>
                复制
              </Button>
              <Button type="primary" ghost onClick={handleDownload}>
                下载
              </Button>
              <Button type="primary" ghost onClick={handleFullScreen}>
                全屏
              </Button>
            </Space>
          </div>

          <TextArea
            value={str}
            style={{ height: "80vh", fontSize: "16px" }}
            onChange={e => {
              setStr(e.target.value);
              setHtml(generateHtmlStr(e.target.value));
            }}
          />
        </div>
        <div className={styles.preview} ref={previewRef}>
          <div className={styles["line-numbers-wrapper"]}>
            {Array(rowNum)
              .fill(0)
              .map((_item, index) => {
                return (
                  <div className={styles["line-number"]} key={index}>
                    {index + 1}
                  </div>
                );
              })}
          </div>
          <div dangerouslySetInnerHTML={{ __html: html }}></div>
        </div>
      </div>
    </>
  );
};

export default JsonParse;

4.2 jsonParse.module.less 样式文件

@attr-color: #92278f;

@str-color: #3ab54a;

@number-color: #25aae2;

@bool-color: #f1592a;

.wrap {
  position: relative;
  display: flex;
  justify-content: space-between;
  margin: 0 auto;
  border-top: 1px solid #696363;
  width: 100%;

  .editor {
    width: 49.5%;
    height: 100%;
    box-sizing: border-box;

    .action {
      display: flex;
      align-items: center;
      height: 40px;
    }
  }

  .preview {
    position: relative;
    overflow: auto;
    overflow-x: hidden;
    margin-top: 40px;
    padding-left: 52px;
    width: 49.5%;
    height: 80vh;
    color: black;
    background: #fff;
    box-sizing: border-box;

    .line-numbers-wrapper {
      position: absolute;
      left: 0;
      width: 30px;
      text-align: center;

      .line-number {
        height: 24px;
        color: #999;
        background: rgb(247 247 247);
        line-height: 24px;
      }
    }

    .json-object {
      position: relative;
      border-left: 1px solid #d0d7de;
    }

    .expand-div {
      user-select: none;
      position: absolute;
      top: -20px;
      left: -20px;
      z-index: 99999;
      display: flex;
      justify-content: center;
      align-items: center;
      width: 18px;
      height: 18px;
      font-size: 14px;
      color: #696969;
      cursor: pointer;
    }

    .json-item {
      margin: 0 16px;
    }

    .prefix-key {
      color: @attr-color;
    }

    .str-value {
      color: @str-color;
    }

    .number-value {
      color: @number-color;
    }

    .bool-value {
      color: @bool-color;
    }

    .null-value {
      color: @bool-color;
    }
  }
}

4.3 is.ts 工具文件

export function isString(value: any): value is string {
  return typeof value === "string";
}

export function isNumber(value: any): value is number {
  return typeof value === "number";
}

export function isBoolean(value: any): value is boolean {
  return typeof value === "boolean";
}

export function isObject(value: any): value is object {
  return value !== null && typeof value === "object";
}

export function isFunction(value: any): value is Function {
  return typeof value === "function";
}

export function isUndefined(value: any): value is undefined {
  return typeof value === "undefined";
}

export function isNull(value: any): value is null {
  return value === null;
}

export function isSymbol(value: any): value is symbol {
  return typeof value === "symbol";
}

export function isBigInt(value: any): value is bigint {
  return typeof value === "bigint";
}

export function isArray(value: any): value is any[] {
  return Array.isArray(value);
}

export function isDate(value: any): value is Date {
  return value instanceof Date;
}

export function isRegExp(value: any): value is RegExp {
  return value instanceof RegExp;
}

export function isError(value: any): value is Error {
  return value instanceof Error;
}

export default {
  isString,
  isNumber,
  isBoolean,
  isObject,
  isFunction,
  isUndefined,
  isNull,
  isSymbol,
  isBigInt,
  isArray,
  isDate,
  isRegExp,
  isError,
};

4.4 fileUtils.ts 工具文件

export default class FileUtils {
  // 通过 url 下载文件
  static downloadFileByUrl(url: string, name: string) {
    const a = document.createElement("a");
    a.href = url;
    a.download = name;
    a.click();
  }

  // 通过 blob 下载文件
  static downloadFileByBlob(blob: Blob, name: string) {
    const a = document.createElement("a");
    a.href = URL.createObjectURL(blob);
    a.download = name;
    a.click();
  }

  // b 转 KB、mb、gb
  static bytesToSize(bytes: number): string {
    if (bytes === 0) return "0 B";
    const k = 1024;
    const sizes = ["B", "KB", "MB", "GB", "TB"];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return (bytes / Math.pow(k, i)).toFixed(2) + " " + sizes[i];
  }
}

4.5 clipboardUtil.ts 工具文件

export default class ClipboardUtil {
  private static clipboard = navigator.clipboard;

  // 读取剪切板内容
  static readText(): Promise<any> {
    return this.clipboard.readText();
  }

  // 向剪切板写入内容
  static writeText(str: string): Promise<any> {
    return this.clipboard.writeText(str);
  }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值