用 Vue.js 实现一个 JSON Viewer

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

常用的 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, "&lt;");
  object = object.replace(/\>/g, "&gt;");
  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点! 睡觉睡觉! 晚安世界!

标签: vue, json, 前端

添加新评论