为了便于用户能直观地看到配置项的一些属性,于是把整个配置以一个 JSON 串的形式展示。本来打算以一个表格或者列表的形式展示,不过配置项中某些属性是 Object 类型,思来想去,还是 JSON 串更直观一点。但是当某个配置改变时,JSON 里的变动不是很明显(如下图所示)。当这个 JSON 里的属性太多时,就不容易能注意到是哪个配置变了。于是,就想能不能做一个属性变化的提示,当某个配置改变时,显示一个高亮的动画什么的,提示用户这个配置改变了 🤔。想法有了,准备冻手!

思路

JSON 串分行,根据当前改变的属性去找到该属性对应的行区间,然后高亮显示这些行。整个流程如下:

Step.One:更改属性

考虑到这个对象的属性太多,专门写个表单去更改,实在太麻烦了。于是,就想了一个方法:随机去更改某个属性。

这里只对最底层的子属性做修改,例如,对于下方的 obj 对象:

const obj = {
  name: "Tom",
  age: 20,
  address: {
    province: "A",
    city: "B",
    county: "C"
  }
}

可以修改的属性有:

  • obj.name
  • obj.age
  • obj.address.province
  • obj.address.city
  • obj.address.county

没有对 obj.address 做更改哦!!!

按照上方罗列的属性,想必你也猜到了,需要对源对象进行「 扁平化 」处理,也即把上述的 obj 对象转成下面这种格式:

const flattenObj = {
  "obj.name": "Tom",
  "obj.age": 20,
  "obj.address.province": "A",
  "obj.address.city": "B",
  "obj.address.county": "C"
}

「 扁平化 」的具体实现,可以参考文末的工具函数 flattenObj

已知当前对象可更改的属性有 5 个,随机取一个属性,那么就可以写出下面这段代码:

const newObj = flattenObj(obj)
const keys = Object.keys(newObj)
const key = keys[Math.floor(Math.random() * keys.length)]

拿到这个 key 后,就可以去做修改了。为了让这个属性更改后的值更具有差异性,可以让更改后的值类型不同于源类型。于是制定了一些更改规则,如下:

  • Array -> Boolean,即 Array 类型更改后为 Boolean 类型,下面以此类推。
  • Boolean -> Number
  • Number -> String
  • String -> Array

例如,对于下方的 obj 对象:

const obj = {
  name: "Tom",
  age: 20,
  student: true,
  likes: ["apple", "banana", "pear"]
}

按照上述规则,更改后的值可能为:

const obj = {
- name: "Tom",
+ name: [1, 2],
  age: 20,
  student: true,
  likes: ["apple", "banana", "pear"]
}

也可能为:

const obj = {
  name: "Tom",
- age: 20,
+ age: "asx",
  student: true,
  likes: ["apple", "banana", "pear"]
}

也可能为:

const obj = {
  name: "Tom",
  age: 20,
- student: true,
+ student: 239,
  likes: ["apple", "banana", "pear"]
}

也可能为:

const obj = {
  name: "Tom",
  age: 20,
  student: true,
- likes: ["apple", "banana", "pear"]
+ likes: true
}

具体实现可以参考文末的工具函数 genOtherTypeValue

已知需要更新的 key 和需要更改的值了,就可以去对源对象做修改了。

因为拿到这个 key 是「 扁平化 」后的,也即是链式的。如果想用这个 key 去更改源对象 obj 中对应的属性,需要做一些处理。用一个工具函数 updateValByChainKey 根据链式的 key 去更新对象中的值,以及 getType 去获取当前的值类型。

完善上面的代码后,如下:

const newObj = flattenObj(obj)
const keys = Object.keys(newObj)
const key = keys[Math.floor(Math.random() * keys.length)]
// 新增
const type = getType(newObj[key])
const newVal = genOtherTypeValue(type)
updateValByChainKey(obj, key, newVal)

源对象已经更新了,接下来就是生成新的 JSON 串,然后分行。

Step.Two:生成 JSON 串,并分行

通过 JSON.stringify(obj, null, 2) 生成 JSON 串。因为要根据行号来添加高亮效果,直接操作整个 JSON 串肯定不行,所以需要对整个 JSON 串进行分行,直接调用 String.split("\n") 就行,展示时,起始行号设为 1 (如下图所示)。

Step.Three:计算行区间

举个例子,colorMultiType 的行区间就是 [3, 13]colorMultiType.start 的行区间就是 [4, 7]colorMultiType.start.color 的行区间就是 [5, 5] 或者 [5]

同样,为了方便记录行区间,也需要进行「 扁平化 」处理,用这个「 扁平化 」后的对象去记录每个属性对应的行区间,获取到各个属性的行区间后的对象 rowObj 如下:

const rowObj = {
  colorCommon: [2],
  colorMultiType: [3, 13],
  "colorMultiType.start": [4, 7],
  "colorMultiType.start.color": [5],
  "colorMultiType.start.offset": [6, 6],
  "colorMultiType.end": [8, 11],
  "colorMultiType.end.color": [9],
  "colorMultiType.end.offset": [10, 10],
  "colorMultiType.direction": [12, 12],
  colorGradient: [14, 19],
  "colorGradient.show": [15],
  "colorGradient.fromColor": [16],
  "colorGradient.toColor": [17],
  "colorGradient.angle": [18, 18],
  colorTextShadow: [20, 24],
  "colorTextShadow.size": [21],
  "colorTextShadow.color": [22],
  "colorTextShadow.show": [23, 23],
  colorGroup: [25, 29]
}

获取行区间的具体实现,可以参考文末的工具函数 getPropRowsFromJSON

Step.Four:高亮显示行区间

Step.One 中,拿到了更改的属性 key ,在 Step.Three 中拿到了每个属性对应的行区间对象 rowObj ,那么需要高亮的行区间 highLightRows = rowObj[key]。区间行的高亮通过 CSS 添加一个高亮 Class 来实现。

<template>
  <div>
    <div v-for="(item, i) in strList" :key="i">
      <div class="line-wrap">
        <span class="line-num">{{ i + 1 }}</span>
        <pre :class="isHighLight(i + 1)">{{ item.text }}</pre>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  data() {
    return {
      strList: [], // 按 "\n" 分割后的 JSON 串数组
      highLightRows: [], // 高亮行区间
    }
  },
  methods: {
    isHighLight(idx) {
      if (!this.highLightRows.length) return '';
      // 因为有些行区间,可能为一个元素或者两个元素
      // 这里判断索引大于等于第一个元素
      // 并且小于等于最后一个元素
      // 这样就都能满足啦
      if (
        idx >= this.highLightRows[0] &&
        idx <= this.highLightRows[this.highLightRows.length - 1]
      ) {
        return 'tip';
      }
      return ''
    }
  }
}
</script>
/* 动画函数 */
@keyframes blingbling {
  0%,
  50%,
  100% {
    background-color: transparent;
  }

  25%,
  75% {
    background-color: orange;
  }
}

.tip {
  animation: blingbling ease 1s;
}

最终效果

总结

  • 这里没有考虑数组元素中有对象的情况,默认数组里都是基础数据类型。
  • getPropRowsFromJSON 这个方法,感觉写的不是很好(虽然写的时候快自闭了),应该有更好的方法 🤔。
  • 已知当前更改的属性了,没有必要把整个对象的所有属性都生成行区间,算是一个可以优化的点。

工具函数

getType
// 获取类型
function getType(obj) {
  return Object.prototype.toString.call(obj).slice(8, -1)
}
isObject
// 判断是否是 Object 类型
function isObject(obj) {
  return getType(obj) === "Object"
}
flattenObj
// 扁平化对象
function flattenObj(obj) {
  const res = {}
  const handler = (obj, prefix = "") => {
    if (prefix) {
      prefix += "."
    }
    for (const key in obj) {
      const newKey = `${prefix}${key}`
      if (isObject(obj[key])) {
        handler(obj[key], newKey)
      } else {
        res[newKey] = obj[key]
      }
    }
  }
  handler(obj)
  return res
}
updateValByChainKey
// 根据链式的 key,更新对象中对应属性的值
function updateValByChainKey(obj, keys, val) {
  const seqs = keys.split(".")
  if (seqs.length === 1) {
    obj[keys] = val
  } else {
    for (let i = 0; i < seqs.length; i++) {
      const key = seqs[i]
      if (i !== seqs.length - 1) {
        obj = obj[key]
      } else {
        obj[key] = val
      }
    }
  }
}
genOtherTypeValue
// 根据当前类型生成其他类型的值
function genOtherTypeValue(type) {
  const genRandomInteger = (max, min = 0) => Math.floor(Math.random() * max) + min
  const ops = {
    Array() {
      return Math.random() < 0.5 ? true : false
    },
    Boolean() {
      return genRandomInteger(1000)
    },
    Number() {
      const len = genRandomInteger(10)
      const res = new Array(len).fill(0).map(() => String.fromCharCode(genRandomInteger(26) + 97))
      return res.join("")
    },
    String() {
      const len = genRandomInteger(5)
      return new Array(len).fill(0).map(() => genRandomInteger(10))
    }
  }
  return ops[type]()
}
getPropRowsFromJSON
// 获取对象转成 JSON 串后,各个属性对应的行区间
function getPropRowsFromJSON(obj, stringify = v => JSON.stringify(v, null, 2)) {
  const flattenedObj = flattenObj(obj)
  const objJson = stringify(obj)
  const objJsonArr = objJson.split("\n")
  const propStack = []
  const spaceStack = [0]
  const rowObj = {}

  for (let i = 1; i < objJsonArr.length - 1; i++) {
    const text = objJsonArr[i]
    const propName = getPropName(text)
    const countSpace = countOfStartSpace(text)
    // 下钻层级
    if (countSpace > spaceStack[spaceStack.length - 1]) {
      spaceStack.push(countSpace)
    } else if (propName) {
      // 上一层级结束
      while (spaceStack.length && spaceStack[spaceStack.length - 1] >= countSpace) {
        spaceStack.pop()
        propStack.pop()
      }
      spaceStack.push(countSpace)
    } else {
      rowObj[propStack.join(".")][1] = i
      const key = propStack.slice(0, -1).join(".")
      if (key) {
        rowObj[key].push(i + 1)
      } else {
        rowObj[propStack.join("")][1] = i + 1
      }
    }
    if (propName) {
      propStack.push(propName)
      const key = propStack.join(".")
      if (Array.isArray(flattenedObj[key])) {
        if (flattenedObj[key].length) {
          rowObj[key] = [i + 1, i + 1 + flattenedObj[key].length + 1]
          i += flattenedObj[key].length + 1
        } else {
          rowObj[key] = [i + 1]
        }
      } else {
        rowObj[key] = rowObj[key] || [i + 1]
      }
    }
  }

  return rowObj

  // 字符串开头空格的数量
  function countOfStartSpace(str) {
    return str.length - str.trimStart().length
  }

  // 获取属性名
  function getPropName(str) {
    const res = str.match(/^\s+\"([^"]+)":/)
    return res ? res[1] : ""
  }
}