演示地址: 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';
};
当然, 样式我也抄了, 折叠看数组长度这个酷炫的想法也抄了哈哈! 折叠展开的实现可以看下函数 show
和 hide
, 原理比较简单: 折叠的时候把 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
: 根据值的类型和缩进层级返回字符串, 如果是 Array
或 Object
, 将会递归调用: 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, 它演示了组件的递归使用, 这次派上用场了! 因为格式化的原理是根据值的类型返回特定的字符串, 结合组件化的思想, 我们递归返回组件就可以了.
如何实现组件化
- JSON由key和val组成, 不妨将它们各自拆分为一个组件;
- JSON的每一行由key:val组成, key:val合并为item组件.
也就是说, item
父组件包含了 key
和 val
子组件, val
有多种类型, 如果是 Array
或 Object
, 递归展开为 item
组件. 至于为什么叫 val
不叫 value
组件, 因为我强迫症啊哈哈哈! 都是3个字母看起来很顺眼.
OK, 瞎哔哔了这么多, 是时候看代码了. 定义(呸)抄一下缩进字符串和类型判断函数:
// 缩进字符串
var padstr = ' ';
// 返回给定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
模板复杂了些. 如果是 Array
或 Object
, 判断当前组件的 open
打开状态, 如果为 true
, 渲染折叠 -
图标并递归渲染 item
组件, 否则渲染展开 +
图标, 并根据类型生成折叠后的字符串; 如果是 Null
, String
, Number
或 Boolean
, 渲染带有样式的 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
组件把 key
和 val
组件合起来就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-depth
为 0
, 表示根节点, 无缩进层级, 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
提供了 getJson
和 setJson
接口, 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/