用 Vue.js 实现一个 JSON Viewer

简介: JSON 是一种轻量级的数据交换格式, 相信大家用得比较多, 平时也用了很多格式化工具, 例如我最常用的 Json.cn, 还有这个 BeJson, 前者清爽无广告, 后者性能给力(有广告). 本文讲详细讲解如何使用 Vue.js 实现一个 JSON Viewer!

演示地址: http://json.imlht.com/vue-json-viewer-demo.html

json-viewer

常用的 JSON 格式化工具

JSON 是一种轻量级的数据交换格式, 相信大家用得比较多, 平时也用了很多格式化工具, 例如我最常用的 Json.cn, 还有这个 BeJson, 前者清爽无广告, 后者性能给力(有广告), 可以复制下面的 JSON 体验一下:

JSON 实例

{
  "name": "小明",
  "age": 24,
  "gender": true,
  "height": 1.85,
  "weight": null,
  "skills": [
    {
      "PHP": [
        "Laravel",
        "Composer"
      ]
    },
    {
      "JavaScript": [
        "jQuery",
        "Vue",
        "React"
      ]
    },
    "Golang",
    "Python",
    "Lua"
  ]
}

窥探 Json.cn 的实现

想自己实现一个 JSON 工具, 偷师是必不可少滴. 翻下 Json.cn 的源码, 发现是用 jQuery 写的, 代码量不多, 比较有用的就是缩进填充函数 indent_tab 还有类型判断函数 _typeof:

function indent_tab(indent_count) {
  return (new Array(indent_count + 1)).join('    ');
}

function _typeof(object) {
  var tf = typeof object,
    ts = _toString.call(object);
  return null === object ? 'Null' :
    'undefined' == tf ? 'Undefined'   :
      'boolean' == tf ? 'Boolean'   :
        'number' == tf ? 'Number'   :
          'string' == tf ? 'String'   :
            '[object Function]' == ts ? 'Function' :
              '[object Array]' == ts ? 'Array' :
                '[object Date]' == ts ? 'Date' : 'Object';
};

当然, 样式我也抄了, 折叠看数组长度这个酷炫的想法也抄了哈哈! 折叠展开的实现可以看下函数 showhide, 原理比较简单: 折叠的时候把 innerHTML 存进 data-inner, 展开的时候再写回去:

function hide(obj) {
  var data_type = obj.parentNode.getAttribute('data-type');
  var data_size = obj.parentNode.getAttribute('data-size');
  obj.parentNode.setAttribute('data-inner',obj.parentNode.innerHTML);
  if (data_type === 'array') {
    obj.parentNode.innerHTML = '<i style="cursor:pointer;" class="fa fa-plus-square-o" onclick="show(this)"></i>Array[<span class="json_number">' + data_size + '</span>]';
  } else {
    obj.parentNode.innerHTML = '<i style="cursor:pointer;" class="fa fa-plus-square-o" onclick="show(this)"></i>Object{...}';
  }
}

function show(obj) {
  var innerHtml = obj.parentNode.getAttribute('data-inner');
  obj.parentNode.innerHTML = innerHtml;
}

再看看函数 format: 根据值的类型和缩进层级返回字符串, 如果是 ArrayObject, 将会递归调用: format -> _format_array -> format -> _format_object -> ...

function format(object, indent_count) {
  var html_fragment = '';
  switch (_typeof(object)) {
  case 'Null':
    0 html_fragment = _format_null(object);
    break;
  case 'Boolean':
    html_fragment = _format_boolean(object);
    break;
  case 'Number':
    html_fragment = _format_number(object);
    break;
  case 'String':
    html_fragment = _format_string(object);
    break;
  case 'Array':
    html_fragment = _format_array(object, indent_count);
    break;
  case 'Object':
    html_fragment = _format_object(object, indent_count);
    break;
  }
  return html_fragment;
};

function _format_null(object) {
  return '<span class="json_null">null</span>';
}

function _format_boolean(object) {
  return '<span class="json_boolean">' + object + '</span>';
}

function _format_number(object) {
  return '<span class="json_number">' + object + '</span>';
}

function _format_string(object) {
  object = object.replace(/\</g, "<");
  object = object.replace(/\>/g, ">");
  if (0 <= object.search(/^http/)) {
    object = '<a href="' + object + '" target="_blank" class="json_link">' + object + '</a>'
  }
  return '<span class="json_string">"' + object + '"</span>';
}

function _format_array(object, indent_count) {
  var tmp_array = [];
  for (var i = 0,
  size = object.length; i < size; ++i) {
    tmp_array.push(indent_tab(indent_count) + format(object[i], indent_count + 1));
  }
  return '<span data-type="array" data-size="' + tmp_array.length + '"><i  style="cursor:pointer;" class="fa fa-minus-square-o" onclick="hide(this)"></i>[<br/>' + tmp_array.join(',<br/>') + '<br/>' + indent_tab(indent_count - 1) + ']</span>';
}

function _format_object(object, indent_count) {
  var tmp_array = [];
  for (var key in object) {
    tmp_array.push(indent_tab(indent_count) + '<span class="json_key">"' + key + '"</span>:' + format(object[key], indent_count + 1));
  }
  return '<span  data-type="object"><i  style="cursor:pointer;" class="fa fa-minus-square-o" onclick="hide(this)"></i>{<br/>' + tmp_array.join(',<br/>') + '<br/>' + indent_tab(indent_count - 1) + '}</span>';
}

递归返回组件

了解原理之后, 再回头想想该如何用 Vue.js 实现? 熟悉 Vue 官方文档的人应该会想到官方实例: 树形视图 Example, 它演示了组件的递归使用, 这次派上用场了! 因为格式化的原理是根据值的类型返回特定的字符串, 结合组件化的思想, 我们递归返回组件就可以了.

如何实现组件化

  1. JSON由key和val组成, 不妨将它们各自拆分为一个组件;
  2. JSON的每一行由key:val组成, key:val合并为item组件.

也就是说, item 父组件包含了 keyval 子组件, val 有多种类型, 如果是 ArrayObject, 递归展开为 item 组件. 至于为什么叫 val 不叫 value 组件, 因为我强迫症啊哈哈哈! 都是3个字母看起来很顺眼.

OK, 瞎哔哔了这么多, 是时候看代码了. 定义(呸)抄一下缩进字符串和类型判断函数:

// 缩进字符串
var padstr = '&nbsp;&nbsp;&nbsp;&nbsp;';

// 返回给定value的类型
function valueType(value) {
  var tf = typeof value;
  var ts = Object.prototype.toString.call(value);
  return value === null ? 'Null' :
    'boolean' === tf ? 'Boolean' :
      'number' === tf ? 'Number' :
        'string' === tf ? 'String' :
          '[object Array]' === ts ? 'Array' : 'Object';
}

什么鬼?! 第一个单词 var, 用 const 啊! 好吧我只是为了说明原理, 所以没有用 ES6/7 等高级特性, 没有 webpack 也没有 npm, 全部被我撸在一个 html 里了哈哈哈!

key 组件

组件 key 逻辑比较简单, key 用双引号 " 包起来, 如果是数组的 key, 那就不渲染. 另外再根据层级填充缩进字符即可:

<!-- key template -->
<script type="text/x-template" id="key-template">
  <span class="key">
    <span v-html="pad"></span><strong class="json_key" v-if="render">"{{jsonKey}}"</strong><template v-if="render">:</template>
  </span>
</script>
<script>
Vue.component('key', {
  template: '#key-template',
  props: ['json-key', 'current-depth'],
  computed: {
    pad: function () {
      return new Array(this.currentDepth+1).join(padstr);
    },
    render: function () {
      return isNaN(this.jsonKey);
    }
  },
});
</script>

val 组件

组件 val 模板复杂了些. 如果是 ArrayObject, 判断当前组件的 open 打开状态, 如果为 true, 渲染折叠 - 图标并递归渲染 item 组件, 否则渲染展开 + 图标, 并根据类型生成折叠后的字符串; 如果是 Null, String, NumberBoolean, 渲染带有样式的 span 标签, 如果不是最后一个元素渲染 , 逗号, 最后再渲染 <br> 标签:

<!-- val template -->
<script type="text/x-template" id="val-template">
  <span class="val">
    <template v-if="canToggle">
      <template v-if="open">
        <!-- Array -->
        <template v-if="type === 'Array'"><i class="fa fa-minus-square-o" @click="toggle"></i>[<br>
          <item class="item" :json-val="jsonVal" :current-depth="currentDepth+1" :max-depth="maxDepth"></item><span v-html="pad"></span>]<template v-if="!last">,</template><br>
        </template>
        <!-- Object -->
        <template v-else-if="type === 'Object'"><i class="fa fa-minus-square-o" @click="toggle"></i>{<br>
          <item class="item" :json-val="jsonVal" :current-depth="currentDepth+1" :max-depth="maxDepth"></item><span v-html="pad"></span>}<template v-if="!last">,</template><br>
        </template>
      </template>
      <template v-else>
        <!-- Array -->
        <template v-if="type === 'Array'">
          <i class="fa fa-plus-square-o" @click="toggle"></i><span class="json_hide">Array[<span class="json_number">{{jsonVal.length}}</span>]</span><template v-if="!last">,</template><br>
        </template>
        <!-- Object -->
        <template v-else-if="type === 'Object'">
          <i class="fa fa-plus-square-o" @click="toggle"></i><span class="json_hide">Object{<span class="json_string">...</span>}</span><template v-if="!last">,</template><br>
        </template>
      </template>
    </template>
    <template v-else>
      <!-- Null -->
      <template v-if="type === 'Null'">
        <span class="json_null">null</span><template v-if="!last">,</template><br>
      </template>
      <!-- String -->
      <template v-else-if="type === 'String'">
        <span class="json_string">"{{jsonVal}}"</span><template v-if="!last">,</template><br>
      </template>
      <!-- Number -->
      <template v-else-if="type === 'Number'">
        <span class="json_number">{{jsonVal}}</span><template v-if="!last">,</template><br>
      </template>
      <!-- Boolean -->
      <template v-else-if="type === 'Boolean'">
        <span class="json_boolean">{{jsonVal ? 'true' : 'false'}}</span><template v-if="!last">,</template><br>
      </template>
    </template>
  </span>
</script>
<script>
Vue.component('val', {
  template: '#val-template',
  props: ['json-val', 'current-depth', 'max-depth', 'last'],
  data: function () {
    return { open: this.currentDepth < this.maxDepth };
  },
  computed: {
    pad: function () {
      return new Array(this.currentDepth+1).join(padstr);
    },
    type: function () {
      return valueType(this.jsonVal);
    },
    canToggle: function () {
      return this.type === 'Array' || this.type === 'Object';
    }
  },
  methods: {
    toggle: function () {
      this.open = !this.open;
    }
  }
});
</script>

item 组件

item 组件把 keyval 组件合起来就OK了:

<!-- item template -->
<script type="text/x-template" id="item-template">
  <span>
    <template v-for="(key, i) in keys">
      <key :json-key="key" :current-depth="currentDepth"></key>
      <val :last="i === keys.length-1"
        :json-val="jsonVal[key]"
        :current-depth="currentDepth"
        :max-depth="maxDepth">
      </val>
    </template>
  </span>
</script>
<script>
Vue.component('item', {
  template: '#item-template',
  props: ['json-key', 'json-val', 'current-depth', 'max-depth'],
  computed: {
    pad: function () {
      return new Array(this.currentDepth).join(padstr);
    },
    type: function () {
      return valueType(this.jsonVal);
    },
    keys: function () {
      return Object.keys(this.jsonVal);
    }
  }
});
</script>

vm 实例

根组件没有 key, 所以 #vm 里面只有一个 val 组件. current-depth0, 表示根节点, 无缩进层级, max-depth 表示初始化之后展示到第几层, 这里设为 3:

<!-- vm -->
<div id="vm">
  <val :json-val="json" :current-depth="currentDepth" :max-depth="maxDepth" :last="true"></val>
</div>
<script>
var vm = new Vue({
  el: '#vm',
  data: {
    currentDepth: 0,
    maxDepth: 3,
    json: {
      "name": "小明",
      "age": 24,
      "gender": true,
      "height": 1.85,
      "weight": null,
      "skills": [
        {
          "PHP": [
            "Laravel",
            "Composer"
          ]
        },
        {
          "JavaScript": [
            "jQuery",
            "Vue",
            "React"
          ]
        },
        "Golang",
        "Python",
        "Lua"
      ]
    }
  },
  methods: {
    getJson: function () {
      return this.json;
    },
    setJson: function (json) {
      this.json = json;
    }
  }
});
</script>

#vm 提供了 getJsonsetJson 接口, getJson 返回当前实例的 JSON 对象, 看起来没什么卵用, 但它治好了我的强迫症; setJson 可以动态改变实例的 JSON 对象, 妈妈我再也不用 F5 刷新了, 按下键盘 F12 进入开发者工具的控制台, 然后 vm.setJson(...) 就可以看效果了.

存在的问题

目前没发现有 bug, 如果有的话麻烦告知, 谢谢! 性能上, 解析比较简单的 JSON 倒是可以, 层级多的或者体积大的 JSON 会特别慢, 可能消耗在递归上. 有兴趣的可以动手测试一下, 欢迎交流.

最后

分享一下自用的 JSON 解析工具, 绿色无广告, 解析速度飞快: http://json.imlht.com/index.html. 看了下时间, 凌晨1点! 睡觉睡觉! 晚安世界!


> 文章来源于本人博客,发布于 2017-12-21,原文链接:https://imlht.com/archives/88/
目录
相关文章
|
4月前
|
JSON JavaScript 前端开发
JavaScript实现字符串转json对象的方法
JavaScript实现字符串转json对象的方法
|
27天前
|
JSON 前端开发 JavaScript
聊聊 Go 语言中的 JSON 序列化与 js 前端交互类型失真问题
在Web开发中,后端与前端的数据交换常使用JSON格式,但JavaScript的数字类型仅能安全处理-2^53到2^53间的整数,超出此范围会导致精度丢失。本文通过Go语言的`encoding/json`包,介绍如何通过将大整数以字符串形式序列化和反序列化,有效解决这一问题,确保前后端数据交换的准确性。
33 4
|
2月前
|
存储 JSON JavaScript
JavaScript JSON
【10月更文挑战第7天】JSON 是 JavaScript 中非常重要的一个数据格式,它为数据的表示和传输提供了一种简单而有效的方式。掌握 JSON 的使用方法和特点,对于开发高质量的 JavaScript 应用具有重要意义。
|
3月前
|
存储 JSON JavaScript
js中JSON的使用
介绍JSON的基本概念和在JavaScript中的使用方式,包括JSON格式的语法规则、使用`JSON.stringify()`和`JSON.parse()`方法进行对象与字符串的转换,以及处理JSON数组数据。
js中JSON的使用
|
2月前
|
JSON JavaScript 前端开发
js如何格式化一个JSON对象?
js如何格式化一个JSON对象?
100 3
|
3月前
|
XML JSON JavaScript
js的json格式
js的json格式
|
3月前
|
存储 JSON JavaScript
JavaScript JSON
JavaScript JSON
36 5
|
4月前
|
移动开发 JavaScript 前端开发
UniApp H5 跨域代理配置并使用(配置manifest.json、vue.config.js)
这篇文章介绍了在UniApp H5项目中处理跨域问题的两种方法:通过修改manifest.json文件配置h5设置,或在项目根目录创建vue.config.js文件进行代理配置,并提供了具体的配置代码示例。
UniApp H5 跨域代理配置并使用(配置manifest.json、vue.config.js)
|
2月前
|
机器学习/深度学习 JSON JavaScript
LangChain-21 Text Splitters 内容切分器 支持多种格式 HTML JSON md Code(JS/Py/TS/etc) 进行切分并输出 方便将数据进行结构化后检索
LangChain-21 Text Splitters 内容切分器 支持多种格式 HTML JSON md Code(JS/Py/TS/etc) 进行切分并输出 方便将数据进行结构化后检索
36 0
|
4月前
|
JSON JavaScript 前端开发
JavaScript JSON
JavaScript JSON