分类 前端 下的文章

webpack打包优化之webpack-bundle-analyzer

一开始以为 webpack-bundle-analyzer 是什么插件...后来才发现, 原来我用vue-cli安装的包, 在构建的时候, 多加个参数就可以了, 它还自动把浏览器打开了...

# build for production and view the bundle analyzer report
npm run build --report

无需配置, 只需一个参数, over...

让你的Vue项目支持scss/sass

回过头看了之前的Vue项目, 原来我用了2年的Vue了~

看了package.json, 有一个是"vue": "^1.0.21"...

现在Vue的最新稳定版本已经是2.5.2咯.

最近 vue init webpack my-project 了一个新项目, 发现webpack的配置变化挺大的. 在main.js引入了scss, 编译失败, 原来是loader没装.

运行下下面的两行命令, 装上loader, 就可以编译通过了.

npm install --save-dev node-sass
npm install --save-dev sass-loader

好吧, 又水了一篇文章哈哈哈.

图片转ASCII字符图案的原理(可调整亮度对比度 宽高度)

来, 先看效果哈哈哈哈!

演示地址: http://ascii-picture.imlht.com/

             "` """        . "`"""""""""""""""""""w$@w"""""""""""""""""""                                      
                   """""""       `""""""""""""$$$$$$$$$00$$0"""""""""""""""""""                                
                            """"""""""""""""$$$$$$$$$$$$$$$$$$$$0""""""""""""""""""""                          
                                    """""""$$$$$$$$$$$$$$$$$$$$$$$$""""""0(""""""""""""""""                    
                                          $$$$$$$$$$$$$$$$$$$$$$$$$$00&0("""""""""""""""""""""""               
                         `               $$$$$$$$$$$$$$$$$$$$$$$$$$$$$&hLLLL(~~"""""""""""""""""""             
""".                             """""" $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@0000000L""""""""""""""`           
""""""""""""""""                     ""0$$$$$$$$$0("(0$$$$$$$$$$$$$$$$$$$$$$$$$$$@&&h0000v"""""""""".          
"""""""""""""""""""""""""".          ""$$$$$$$$0"""""v00$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$000(""""""""            
"""""""""""""""""""""""""""""""""""""""$$$$$$0""""""""""(00h$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$w0"""               
"""""""""""""""""""""""""""""""""""""""$$$$00$$0""($$$$$$$"""$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$h"            
""""""""""""""""""""""""""""""""""""""0$$$$$$$$0"v$$$$0"(&0"""$$$$$$$$$$$$$&""$$$$$$$0""h$$$&h0w&$$$$"         
"""""""""""""""(vv~~"""""""""""""""""00&$$"0""0&0"$$$$$$$""""""$$$$$$$$$$$$$""$"$$$$$v0""$0$$$$$h$$$$$""""w"""0
"""""""""""""""0"0(0000v"0000"""""""0w0w$$""$$$""""""v""~"""`"""$$$$$$$$$$$(""""$$$$$$$""00"0$$h$&"~$$v""""""$@
   """""""""""("0"000"0"0000v"""""""0$$w$0 ""$"""\` ""      """""$$$$$$$$$$$""\`"""$$$$$$$0""0$$$($$(&$h"""""""$h
       """"""""~"""""0(0v0"00"""""""L$$h0     \`   ""0"    .""""""0$$$v$$$$$""  "0""$$$$0"""w$$$$$w"~""`  """ @"
      """"""""""""~"""0v0000"""""""""$h&$    0 """"  $$""""""""""w$$0"$$$$$""""""0"" " """"v$$$$$$ .       " w"
      """""""""""0""""""""0""""""""""0$$@"""$$$$$$$$$""0&@&&"""""&0""$$$$$$$$$$$$$$$$$0"""L0"&$@""         " 0L
      ""  """"""""(0"""L~"""""""""""""0w$""L0"""$$" `"~0$$$&"""""$$$$$$$$$&$$$$$$$$$$$$$$$$$$"@""             "
      """""""""""""0""""""""""""""""""""""""""($$$$$$$00$hL~"""""$$$$$$$$$w@$$$$$$$$$$&$$$$$$0$&"              
""""""""""""""""""""""""""""""""""""""""""""""$$0@$$$$0w&0~vw&hwh@$$$$$$$""h$$ $$ $$$$"$$$$$~$"@"              
 """"""""""""""""""""""""""""""""""""""""""""" v"""""~"0h0$$$$@~v($$$$$$$""w$$$$   $$$0"$$$$&"h$"              
"""""""""""""""""""""""""""""""""""""""""""""""0"(@$$$wh&$$$$$@v""~$$$$$""""$$$$$jj$$$ "$$$$. 0 "      ;       
""""""""""""""""""""""""""""""""""""""""""` """~$$$0"""(&$$$$$$$"""$$$"     0$$$$$$$$$$ 0$$$$" ""     ;$$;"""""
"""""""""""""""""""""""""""""""""""""""""""" """""""L0$$$$$$$$$$$0""$               "L00w$$$$$.$L" """"$$,"""""
""""""""""""""""""""""""""""""""""""""""""""` "0w$$$$$$$$$$$$$$$$@"""                           "" """"$$$"$"`"
""""""""""""""""""""""""""""""""""""""""""""""  ""$$$Lh$$$$$$$$$$$&""                             "("0@@"""""""
"""""""""""""""""""""""""""""""""""""""""""""""   ." ""&$$$$$$$$$$$$$                              $$0"$$""""""
""""""""""""""""""""""""""""""""""""""""""""""""  .   "&$$$$$$$$$$$$$                              $$$$$$$""("$
"""""""""""""""""""""""""""""""""""""""""""""""""     "L$$$$$$$$$$$$w                               $$$$$$"@$$$
"""""""""""""""""""""""""""""""""""""""""""""          "$$$$$$$$$$$$"                                &$$$""@$$$
""""""""""""""""""""""""""""""""""""""""0""             $$$$$$$$$$$$                                 "$$~"""$$$
"""""""""""""""""""""""""""""""""""""""""              ."$$$$$$$$$$h                                  &$,$##$$0
"""""""""""""""""""""""""""""""""""""""                 ""$$$$$$$$@"                                   0~$-,$$$
"""""""""""""""""""""""""""""""""""""            ".     ..$$$$$$000"                         "     "   "$h""$0$
"""""""""""""""""""""""""""""""""""              ""        $$$$0h0$                         "" ""  "    $$$ """
""""""""""""""""""""""""""""""""""               ""        "$$$$$$$          "              """"  ""    $$$$0""
"""""""""""""""""""""""""""""""""""       "      ""         $$$$$$$         "              """  """     "$$$""0
"""""""""""""""""""""""""""&"""""""              ""         "$$$$$$       ""              ."".  .".      $$$00h

平时看代码会看到很多标点符号的字符拼起来的图案, 特别有趣, 像kong(一个高性能API网关), 除了源代码里面有图案, 命令行也藏了彩蛋:

Kong, the biggest ape in town

    /\  ____
    <> ( oo )
    <>_| ^^ |_
    <>   @    \
   /~~\ . . _ |
  /~~~~\    | |
 /~~~~~~\/ _| |
 |[][][]/ / [m]
 |[][][[m]
 |[][][]|
 |[][][]|
 |[][][]|
 |[][][]|
 |[][][]|
 |[][][]|
 |[][][]|
 |[][][]|
 |[|--|]|
 |[|  |]|
 ========
==========
|[[    ]]|
==========

上面这个图案, 只是停留在外形轮廓上, 而我今天要玩的会深入一点: 基于图片的灰度值来生成图案. 此时的图片不单单有轮廓, 还有光影效果, 也就是素描中提及的黑白灰.

原理实际上挺简单的, 在白色背景下, 字符 $ 会有比较大面积的黑, 而字符 + 相对就淡了很多, 毫无疑问, 空格就是纯白了. 所以, 只要把一些字符按照 , , 排序, 并把这些字符映射为 0-255 的灰度值, 就可以根据图片生成更生动的字符画了.

至于这些字符按照灰度排序, 已经有人帮我们做好了, 具体可以查看这个Demo, 是用 Python 写的:

ascii_char = list("$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. ")

看到这里, 是时候拿起 Python 干起来了! 可以照着链接在自己电脑跑一下, 制作一些白色背景的表情包, 但如果是照片的话会发现很糊, 根本看不清, 于是我拿出神器 Photoshop 调整了 亮度对比度, 尽量调高点, 生成的图案会清晰一些.

每次都去 Photoshop 调整真是繁琐, 每次失败了, 得重新用命令行生成, 然后看生成的图案怎么样, 一直重复这个步骤...而且宽度和高度都需要手工指定...所以萌生了这个想法: 把这些重复繁琐的操作, 交给界面去处理好了! 所以后面的代码都是用 JavaScript 实现的.

OK, 我们先扯回来, 说下灰度的映射算法, 也是很容易理解的, 上面的字符一共有 69 个, 0-255 一共有 256 个字符, 计算出比率 ratio 然后直接把字符取出来即可:

/**
 * ASCII Charset
 *
 * @type {String}
 */
const charset = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. ";

/**
 * 69/256
 *
 * @type {Number}
 */
const ratio = charset.length / 256;

/**
 * 颜色值转换为 ASCII 字符
 *
 * @param  {Number}   r        R
 * @param  {Number}   g        G
 * @param  {Number}   b        B
 * @param  {Number}   a        A
 * @param  {Number}   type     类型
 * @return {String}            ASCII 字符
 */
export const rgba_to_char = (r, g, b, a, type) => {
  if (a === 0) return ' ';
  r = Math.round(a / 255 * r);
  g = Math.round(a / 255 * g);
  b = Math.round(a / 255 * b);
  return charset[ Math.round( ratio * rgb_to_gray(r, g, b, type) ) ] || ' ';
};

根据灰度生成字符, 那灰度怎么来的? 扒了挺多资料, 总体来说有几个公式, 具体可以看这篇文章

Gray = R*0.299 + G*0.587 + B*0.114

上面的 Python 代码用的是这个公式, 参考知乎:

Gray = 0.2126 R' + 0.7152 G' + 0.0722 B'

还有另一种, 这个是我实验后发现的, 用这个方法生成的图案细节会多一些, 大家也可以试试看. 算法是比较复杂的, 基本原理是将 RGB 色彩转为 XYZ 色彩, 再从 XYZ 转到 Lab. Lab颜色空间中的L分量用于表示像素的亮度, 最小值是0(纯黑), 最大值是100(纯白), 而a表红绿, b表黄蓝. 我们需要的是灰度值算法, 所以只需L分量就可以了.

再加上平均值, 最大值, 只取绿色通道, 一共就有6种算法, 代码实现如下:

/**
 * 颜色值转换为灰度
 *
 * @param  {Number} r    R
 * @param  {Number} g    G
 * @param  {Number} b    B
 * @param  {Number} type 类型
 * @return {Number}      灰度值
 */
const rgb_to_gray = (r, g, b, type) => {
  switch (type) {
    case 1:
      return g;
    case 2:
      return Math.max(r, g, b);
    case 3:
      return Math.round((r + g + b) / 3);
    case 4:
      return Math.round(0.299 * r + 0.587 * g + 0.114 * b);
    case 5:
      return Math.round(0.2126 * r + 0.7152 * g + 0.0722 * b);
    case 6:
      // https://github.com/antimatter15/rgb-lab/blob/master/color.js
      // https://github.com/markusn/color-diff/blob/master/lib/convert.js
      r /= 255;
      g /= 255;
      b /= 255;
      r = (r > 0.04045) ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92;
      g = (g > 0.04045) ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92;
      b = (b > 0.04045) ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92;
      let x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047;
      let y = (r * 0.2126 + g * 0.7152 + b * 0.0722) / 1.00000;
      let z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883;
      x = (x > 0.008856) ? Math.pow(x, 1 / 3) : (7.787 * x) + 16 / 116;
      y = (y > 0.008856) ? Math.pow(y, 1 / 3) : (7.787 * y) + 16 / 116;
      z = (z > 0.008856) ? Math.pow(z, 1 / 3) : (7.787 * z) + 16 / 116;
      return Math.round(255 / 100 * ((116 * y) - 16));
  }
};

OK, 目前我们已经实现了彩色的像素值变成ASCII字符, 接下来要解决一个问题, 调整图像的亮度和对比度, 同样也是有公式的, 参考链接:

bitmap() {
  return this.data.map((x, i) => {
    if ((i+1) % 4 === 0) {
      // alpha
      return x;
    }
    // http://blog.csdn.net/hbaizj/article/details/17376857
    const B = this.brightness / 100;
    const c = this.contrast / 100;
    const k = Math.tan( (45 + 44 * c) / 180 * 3.1416 );
    return [x - 127.5 * (1 - B)] * k + 127.5 * (1 + B);
  });
}

最后, 我们只需把用户选择的图片, 转换为 RGB 值, 加上亮度对比度, 宽度高度的变换, 就大功告成了:

onchange() {
  const files = document.getElementById('file').files;
  if (!files || files.length === 0) return;
  const that = this;
  let fr = new FileReader();
  fr.onload = function (event) {
    let img = new Image();
    img.onload = function () {
      let c = document.createElement('canvas');
      if (!that.width && !that.height) {
        that.width = img.width;
        that.height = img.height;
      } else if (!that.width) {
        that.width = Math.round(img.width * (that.height / img.height));
      } else if (!that.height) {
        that.height = Math.round(img.height * (that.width / img.width));
      }
      c.width = that.width;
      c.height = that.height;
      let ctx = c.getContext('2d');
      ctx.drawImage(img, 0, 0, that.width, that.height);
      that.data = ctx.getImageData(0, 0, that.width, that.height).data;
    }
    img.src = event.target.result;
  }
  fr.readAsDataURL(files[0]);
}

完整的源码, 我放到 GitHub 上了, 求Star求Star求Star! 代码是用 Vue2 写的(上面的代码都是再里面摘出来的), 结合了饿了么前端框架做界面, 目前先这样, 有时间再调整下界面吧.

演示地址: http://ascii-picture.imlht.com/

用 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('&nbsp;&nbsp;&nbsp;&nbsp;');
}

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点! 睡觉睡觉! 晚安世界!