记录对一款JSON编辑器的研究
因为后台管理系统有编辑后端JSON配置的需求,于是接触了一款JSON编辑器,目前已经有10.7k的点赞,而且在持续的更新维护,vsCode里面一些关于JSON的插件也是基于这款编辑器。但是git仓库里面关于编辑器的使用例子和文档全是英文,十分不方便。于是写下本文记载了JSON编辑器的一些api的使用,基本上能满足一些简单场景下的需求吧,更多详情可以在其git仓库的例子里面自行查询。(如有错误,请指正!)
json编辑器在线地址:jsoneditoronline.org/#right=loca...
一.先完成一个最简单的json编辑器
typescript
//这里以react为例
import 'jsoneditor/dist/jsoneditor.css';//引入编辑器样式,也可以自定义样式,稍后会讲到
import { useEffect, useState } from 'react';
import JSONEditor, { JSONEditorMode, JSONEditorOptions } from 'jsoneditor';
interface MyJsonEditorProps{
json:json,
onChange:Function,
options:object
}
function MyJsonEditor(props:any){
//获取容器,也可以采用useRef()
let [container, setContainer] = useState<HTMLDivElement | null | HTMLElement>(
null,
);
//用来保存初始化创建的json编辑器
let [jsonEditor, setJsonEditor] = useState<any>(null);
//监听容器变化,当获取到容器时候进行初始化
useEffect(()=>{
console.log("获取容器,",container)
if(container!=null&&jsonEditor==null){
console.log("获取到容器了,",container)
//进行初始化,初始化配置可组件使用者自行传入
//这里示例,具体有哪些配置及其使用方法后面会讲
let options={
history:true,//撤销,前进功能
mode:"tree"//默认json数据展示模式
}
//setState异步执行所以此处无法获取到jsonEditor注入数据的话可对jsonEditor进行监听
setJsonEditor(new JSONEditor(container, options));
}
},[container])
//监听编辑器初始化和传入的json值
useEffect(()=>{
if(!jsonEditor) return//编辑器不存在直接返回
let parsedJson //直接传入的json值需要先将其解析后注入到编辑器
//使用tryCatch,防止传入不是json数据解析报错
try {
parsedJson = JSON.parse(props.json);
} catch (error) {
parsedJson = props.json;
console.log("解析失败,请传入正确的数据格式!")
}
//给编辑器注入初始值!!!!
jsonEditor.set(parsedJson)
return ()=>{
//卸载组件时销毁editor
jsonEditor.destroy()
}
},[jsonEditor,props.json])
return <>
<div
id=jsoneditor
className="jsoneditor-react-container"
ref={(elem) => setContainer(elem)}
/>
</>
}
二.jsonEditor配置详解(配置在编辑器初始化时传入)
1.操作前进后退功能
bash
//如果你担心进行了一些错误操作无法恢复,但又不想刷新重写,那么就加上此配置吧
let options={
history:
}
2.数据默认展示格式
perl
//你想让你的数据被解析成何种格式展示呢?那就加上此配置吧
let options={
mode:"text"//数据将会以文本格式展示
}
//就像下面这样
// [
// {
// "valveKey": "valve_core_request",
// "enable": true,
// "isMerge": false,
// "order": 0,
// "paramsMatchRules": []
// },
// {
// "valveKey": "valve_org_domain_team_doctor",
// "serviceType": "teamDoctor_queryPhoneList",
// "operationType": "queryOne",
// "enable": true,
// "isMerge": false,
// "order": 10
// },
// ]
3.多种模式之间自由切换
csharp
//一种模式太单调了?别慌,那就加上此配置,多种模式自由切换
let options={
modes:[
'code',
'form',
'text',
'tree',
'view',
'preview',
]
}
// 在代码模式中,JSON文档被呈现为正则文本,这对开发人员来说是最熟悉的。
// 在这种模式下,您可以看到实际的原始JSON文本,带有白色间距、缩进和所有分隔符,如双引号、逗号和分号。
// 您可以编辑JSON并格式化或压缩它。
// 在树模式中,JSON在高级JSON编辑器中呈现,重点关注数据内容,而不是每个逗号和双引号。
// 这种模式非常有用,尤其是在处理大型JSON文档时,可以轻松地复制、粘贴、转换或提取其中的部分。
// 树模式还支持突出显示两个JSON文档之间的差异。
4.自定义字段是否可编辑(适用于tree,text,code模式下)
在tree 模式下该回调方法将会将node节点作为第一个参数返回,可以通过返回布尔值限定该字段和值是否可以编辑,也可以返回对象: {field: boolean, value: boolean} 来细致化的限定字段名和字段值是否可以编辑
typescript
//虽然前进后退可恢复,但直接让不需要修改的重要字段不可变更加放心,从此领导再也不用担心我手抖改错配置了
const options={
onEditable:customFieldIsEditable
}
interface nodeProps:{
field:string //字段名称
path:null|Array //字段路径,如果是数组,则路径层级为数组元素从左到右
value:any //当前字段值
}
//example
let exNode={
id:1, [
name:'yfh',
hobby:'lalalalala'
}
function customFieldIsEditable(node){
switch (node.field) {
case 'id' :
return false;
case 'name':
return {
field:false;//定义字段名不可变
value:true; //定义字段值可变
}
default:
return true
}
}
5.自定义编辑器样式
##节点
节点是构成Form、Tree和View模式中分层JSON显示的基本单元。可以是
使用反映其类型和状态的几个类进行自定义。
jsoneditor-field
:属性名称jsoneditor-value
:属性的值
-value元素将根据其类型具有以下类之一:
jsoneditor-null
-
jsoneditor-undefined
jsoneditor-number
jsoneditor-string
jsoneditor-string jsoneditor-color-value
jsoneditor-boolean
jsoneditor-regexp
jsoneditor-array
jsoneditor-object
jsoneditor-url
jsoneditor-is-default
:当值与模式中的默认值匹配时,应用于value元素jsoneditor-is-not-default
:当值与模式中的默认值不匹配时应用于value元素jsoneditor-schema-error
:警告图标
css
/*什么?原生样式太丑了,UI不让通过,那就自己来定义,改到他满意为止*/
/* 暗黑模式,将下面样式复制进css文件,并且引入到组件文件即可,
具体如何定义可在浏览器开发者模式下自行查看元素名称 */
div.jsoneditor,
div.jsoneditor-menu {
border-color: #4b4b4b;
}
div.jsoneditor-menu {
background-color: #4b4b4b;
}
div.jsoneditor-tree,
div.jsoneditor textarea.jsoneditor-text {
background-color: #666666;
color: #ffffff;
}
div.jsoneditor-field,
div.jsoneditor-value {
color: #ffffff;
}
table.jsoneditor-search div.jsoneditor-frame {
background: #808080;
}
tr.jsoneditor-highlight,
tr.jsoneditor-selected {
background-color: #808080;
}
div.jsoneditor-field[contenteditable=true]:focus,
div.jsoneditor-field[contenteditable=true]:hover,
div.jsoneditor-value[contenteditable=true]:focus,
div.jsoneditor-value[contenteditable=true]:hover,
div.jsoneditor-field.jsoneditor-highlight,
div.jsoneditor-value.jsoneditor-highlight {
background-color: #808080;
border-color: #808080;
}
div.jsoneditor-field.highlight-active,
div.jsoneditor-field.highlight-active:focus,
div.jsoneditor-field.highlight-active:hover,
div.jsoneditor-value.highlight-active,
div.jsoneditor-value.highlight-active:focus,
div.jsoneditor-value.highlight-active:hover {
background-color: #b1b1b1;
border-color: #b1b1b1;
}
div.jsoneditor-tree button:focus {
background-color: #868686;
}
/* coloring of JSON in tree mode */
div.jsoneditor-readonly {
color: #acacac;
}
div.jsoneditor td.jsoneditor-separator {
color: #acacac;
}
div.jsoneditor-value.jsoneditor-string {
color: #00ff88;
}
div.jsoneditor-value.jsoneditor-object,
div.jsoneditor-value.jsoneditor-array {
color: #bababa;
}
div.jsoneditor-value.jsoneditor-number {
color: #ff4040;
}
div.jsoneditor-value.jsoneditor-boolean {
color: #ff8048;
}
div.jsoneditor-value.jsoneditor-null {
color: #49a7fc;
}
div.jsoneditor-value.jsoneditor-invalid {
color: white;
}
6.设置为仅读模式
javascript
//如何设置为编辑器仅读
let options={
onEditable:function(node){
if(!node.path){
//在树模式和文本模式下,节点为空,没有路径,字段,值,返回false使文本区仅读
return false
}
return true
}
}
7.自定义插入模版
csharp
//什么?你还在一个一个的创建对象?还在一个一个的添加属性?那几百个对象,几千条属性,你得做到猴年马月去啊?
//来学学如何自定义模版吧,直接插入,效率拉满啦
let options={
templates: [
{
text: 'Person',
title: 'Insert a Person Node',
className: 'jsoneditor-type-object',
field: 'PersonTemplate',
value: {
'firstName': 'John',
'lastName': 'Do',
'age': 28
}
},
{
text: 'Address',
title: 'Insert a Address Node',
field: 'AddressTemplate',
value: {
'street': '',
'city': '',
'state': '',
'ZIP code': ''
}
}
]
}
使用示例:



8.自定义节点className(仅限tree,form,view模式下)
javascript
//在实际使用编辑器的过程中,我们如何自定义节点className呢?
const options={
onClassName:customClassName
}
function customClassName(node){
console.log(node);
return 'customClassName';
}
让我们打印看看node里面都有些什么:

可以看到在编辑器的渲染过程中,会把所有的数据节点都遍历一遍,在经过这个节点以后,暴露出了节点的字段名 ,字段值,节点路径(按照数组元素从左到右依次取值,例如:sourceJson[path[0]][path[1]]...), 而onClassName这个配置项则是需要我们传递一个在遍历到该节点时候执行的方法,并且将该方法的返回结果作为该节点的className,而在css中我们提前定义好相关className的样式,这样就可以做到自己定义节点的css样式,做不同节点样式上的差异化处理
10.动态添加schema对数据字段进行自定义规则校验
css
const schema = {
title: "Employee",
description: "Object containing employee details",
type: "object",
properties: {
firstName: {
title: "First Name",
description: "The given name.",
examples: ["John"],
type: "string",
},
lastName: {
title: "Last Name",
description: "The family name.",
examples: ["Smith"],
type: "string",
},
gender: {
title: "Gender",
enum: ["male", "female"],
},
availableToHire: {
type: "boolean",
default: false,
},
age: {
description: "Age in years",
type: "integer",
minimum: 0,
examples: [28, 32],
},
job: {
$ref: "job",
},
},
required: ["firstName", "lastName"],
};
const job = {
title: "Job description",
type: "object",
required: ["address"],
properties: {
company: {
type: "string",
examples: ["ACME", "Dexter Industries"],
},
role: {
description: "Job title.",
type: "string",
examples: ["Human Resources Coordinator", "Software Developer"],
default: "Software Developer",
},
address: {
type: "string",
},
salary: {
type: "number",
minimum: 120,
examples: [100, 110, 120],
},
},
};
const json = {
firstName: "John",
lastName: "Doe",
gender: null,
age: "28",
availableToHire: true,
job: {
company: "freelance",
role: "developer",
salary: 100,
},
};
export const options = {
schema: schema,
schemaRefs: { job: job },
mode: "tree",
modes: ["code", "text", "tree", "preview"],
};
可以通过编辑器初始化时候传入schema对字段进行校验,也可以通过编辑器引用的setSchema 方法动态添加校验规则,具体校验规则的语法可以参考:json-schema.org/
11.监听数据变化
javascript
const options={
onChange:onChange,
onChangeJson:onChangeJson
}
function onChange(){
}
function onChangeJson(cureentJson){
console.log(currentJson)//这是最新的json数据
}
(1).onChange:
onchange在编辑器处于任何模式下数据发生变化时都会被触发,在这里最显而易见的就是可以通过此回调函数监听数据变化,获取到最新的修改数据,注意此回调函数没有任何参数, 可以通过编辑器的get()(注意在'text','code','preview'模式下使用get()方法会抛出异常) 或者getText() 方法获取到最新数据,而且编辑器更改数据的方法:set(),setText,update(),updateText()并不会触发onChange
(2)onChnageJSON:
此方法也是监听数据变化的回调函数,不同之处在于此方法只会在 'tree','form','view' 三种模式下数据发生变化时被触发,并且传递最新的json数据作为第一个回调参数,此参数的数据格式为document,而且编辑器更改数据的方法:set(),setText,update(),updateText()并不会触发onChange
(3)onChangeText:
此方法也是监听数据变化的回调函数,此方法会在任何模式下数据发生变化时被触发,并且传递最新的json数据作为第一个回调参数,此参数的数据格式为被stringify了的json,而且编辑器更改数据的方法:set(),setText,update(),updateText()并不会触发onChange
可以看此下面展示在数据发生变化时打印的各自参数:

12.监听节点展开或者折叠(仅限tree,form,view模式)
bash
const options={
onExpand:onExpand
}
function onExpand({path,isExpand,recursive}){
//path:当前节点路径
//isExpand:当前节点是否展开,展开为true,折叠为false
//recursive:是否递归展开,即一次性展开父节点及其子节点时候为true,此参数会在expandAll时为true
}
13.监听编辑器模式的改变
javascript
const options={
onModeChange:modeChange
}
function modeChange(newMode,oldMode){
}
14.自定义节点名称
typescript
const options={
onNodeName:customNodeName
}
function customNodeName({path,type,size,number,value}){
}
{
path: string[],
type: 'object' | 'array',
size: number,
value: object
}

15.实现一边编辑一边预览
思路讲解:
(1).既然要一边编辑json,一边预览json,那么就需要初始化两个编辑器添加给容器,一个用于编辑,一个用于预览
(2)第二步那就是要实现数据同步,我们接收传入进来的props里面的json数据,并且在初始化时候注入给编辑和预览编辑器,同时需要在用来编辑的编辑器的初始化options配置中配置onChange回调方法,此方法会在数据发生变化时被调用,这个时候我们只需要拿到来编辑的编辑器引用,调用他的get方法就可以获取到最新的json数据,然后调用预览编辑器的set方法,将最新的json数据注入到预览编辑器即可,需要注意的是当编辑器不包含有效的JSON时,此方法抛出异常,当编辑器处于"代码"、"文本"或"预览"模式时,可能会出现这种情况。所以我们可以监听模式变化,onModeChange(newMode),这样的话就可以通过当前模式来判断使用get()还是getText(),下面是打印出来的两种方法获取的数据格式

16.实现差异数据高亮显示
使用场景:嗯?要我修改json格式的数据,那还不简单,看我引入jsonEditor包,再自己封装一个json编辑器,噼里啪啦一顿操作,完事......emm,我刚才修改哪里了,有没有改到不该修改的位置啊?不慌,现在再实现个新功能,让我们将老数据和新数据的差异来个高亮显示,再也不用担心搞错啦!
思路讲解:上面有讲到我们可以在初始化时候配置选项中通过onClassName自定义节点的className,所有我们可以传入一个函数在经过每个节点时候取出两个json数据中该字段的值,并且对取出来的两个值进行对比,如果值相同就返回一个定义值相同时样式的className字符串,否则返回一个定义值不相同时候的className字符串,并且在css中定义这两个className的样式,从而做到高亮显示,而在值发生变化的情况下,这个时候值没有发生变化的编辑器是不会重新渲染的,这个时候我们可以通过调用另外一个编辑器的 refresh 方法强制重新渲染,而如果想做到动态双向绑定效果,我们可以通过监听编辑器的onChange事件,在监听当前编辑器发生变化时,动态调用另外一个编辑器的refresh方法刷新另外一个编辑器,从而我们就不用手动通过按钮控制编辑器刷新对比数据
javascript
const options={
onClassName:customClassName,
onChange:jsonChange
}
function customClassName(node){
const {field,value,path}=node//这里三个值分别是当前节点字段名,字段值,和字段在json数据中的路径
//获取两个编辑器的json数据
let leftJson=leftJsonEditor.get()
let rightJson=rightJsonEditor.get()
//通过路径取出两遍的值,或者直接比较value
let leftFieldValue=getFieldValue(leftJson,path)
let rightFieldValue=getFieldValue(rightJson,path)
return isEqual(leftFieldValue,rightFieldValue)?'the-same-value':'the-different-value'
}
function getFieldValue(source,path){
//todo...
}
function isEqual(lVal,rVal){
//todo...
}
function jsonChange(){
//todo...
}
下面是我打印出来的onClassName的参数,可以看到是不能直接对两个值是否相等进行直接的比较,因为如果值可能是引用数据,这种情况下可以选择对数据进行递归比较,也可以选择直接跳过就可以了,因为接下来还会编辑这两个对象的属性节点,相比来说这样更加节省性能,并且相同的路径能取出来类型相同的引用类型数据的话基本就没有在比较的意义了,后面比较他们的属性即可
同时也可能会遇见null数据类型,对于这些的特殊情况,在进行比较时都需要注意

17.如何自定义菜单项

javascript
const options = {
//onCreateMenu允许我们注册一个回调函数来自定义
//上下文菜单。回调包含两个参数,items和path。
//Items是一个数组,包含当前菜单项和路径
//(如果存在)包含当前节点的路径(作为数组)。
//回调应返回菜单的修改(或未修改)列表
//项目。
//每当用户单击上下文菜单按钮时,菜单
//从零开始创建,并调用此回调。
onCreateMenu: function (items, node) {
const path = node.path
//让我们将当前菜单项和选中节点打印出来看看
console.log('items:', items, 'node:', node)
//我们将添加一个返回当前节点路径的菜单项
//作为jq路径选择器(https://stedolan.github.io/jq/)。首先我们
//将创建一个函数,然后我们将此函数连接到
//菜单项单击属性。
function pathTojq() {
let pathString = ''
path.forEach(function (segment, index) {
if (typeof segment == 'number') {
pathString += '[' + segment + ']'
} else { // ... or object keys
pathString += '."' + segment + '"'
}
})
alert(pathString) // show it to the user.
}
//创建一个新的菜单项。以我们为例,我们只想这样做
//如果有路径(在appendnodes的情况下(对于新对象)
//在创建节点之前,路径为空)
if (path) {
//项目阵列中的每个项目表示一个菜单项目,
//并且需要以下详细信息:
items.push({
text: '我的菜单项', //菜单项的文本
title: '显示此节点的jq路径', //HTML title属性
className: 'example class', //菜单项的css类名
click: pathTojq //单击菜单项时要调用的函数
})
}
//现在我们将遍历菜单项,其中包括
//由jsoneditor创建,以及我们在上面添加的新项目。
//例如,我们将只更改项的className属性,但是
//您可以更改任何属性(例如单击回调、文本属性等)
//对于任何项目,甚至删除整个菜单项。
items.forEach(function (item, index, items) {
if ("submenu" in item) {
//如果项目具有子菜单属性,则它是子菜单标题
//并且包含另一个菜单项阵列。让我们上色
//那个黄色的。。。
items[index].className += ' submenu-highlight'
} else {
// if it's not a submenu heading, let's make it colorful
items[index].className += ' rainbow'
}
})
//注意,上面的循环不是递归的,所以它只更改类
//在顶级菜单项上。还要处理子菜单中的菜单项
//如果项目有一个"子菜单"数组,则应该遍历该数组。
//接下来,让我们删除所有菜单分隔符(同样位于
//顶级菜单)。菜单分隔符是一个类型为"separator"的项
//财产
items = items.filter(function (item) {
return item.type !== 'separator'
})
//最后,我们需要返回items数组。如果我们不这样做,菜单
//将为空。
return items
}
}
18.转换数据:enableTransform
使用[JMESPath]启用筛选、排序和转换JSON查询。仅适用于模式"树"true
默认情况下。更多请查看:jmespath.org/
arduino
const options={
enableTransform:true//允许
}