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 { "<": "<", ">": ">", "&": "&", '"': """ }[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 { "<": "<", ">": ">", "&": "&", '"': """ }[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);
}
}