"admin" 发布的文章

现在在高铁上, 赶着春节回家过年, 无座站票, 电脑只能放行李架上, 面对着行李架撸键盘--看过<Redis的设计与实现>这本书, 突然想起, 便整理下SDS的内容, 相对后面的章节, 算是比较简单的~

大多数情况下, Redis使用SDS(Simple Dynamic String, 简单动态字符串)作为字符串表示, 比起C字符串, SDS具有以下优点:

  1. 常数复杂度获取字符串长度;
  2. 杜绝缓冲区溢出;
  3. 减少修改字符串时带来的内存重分配次数;
  4. 二进制安全;
  5. 兼容部分C字符串函数.

以上列举的优点, 也算是这篇文章的主要内容. 先从定义说起吧:

1. SDS的定义

SDS结构定义如下(sds.h/sdshdr):

struct sdshdr {
    // 记录buf数组中已使用字节的数量, 等于SDS所保存字符串的长度
    int len;
    // 记录buf数组中未使用字节的数量
    int free;
    // 字节数组, 用于保存字符串
    char buf[];
}
  1. len属性记录了已使用的字节数量(字符串长度);
  2. free属性的值为0, 表示这个SDS没有未使用的空间;
  3. free属性的值为5, 表示这个SDS保存了一个5字节长的字符串;
  4. buf属性是一个char类型的数组, 数组的最后一个字节保存了空字符0.

SDS遵循C字符串以空字符串结尾的惯例, 空字符不计入SDS的len属性, 即额外为空字符分配了1字节的空间, 并且添加空字符到字符串末尾均由SDS函数自动完成, 对使用者完全透明. 该特性带来的好处是, SDS可以直接复用C字符串函数库的部分函数.

2. SDS与C字符串的区别

2.1 常数复杂度获取字符串长度

  1. 由于C字符串不记录自身长度, 所以获取长度时需要遍历整个字符串, 直到遇到空字符0为止, 该操作的复杂度为O(N);
  2. 由于SDS在len属性中记录了SDS本身的长度, 所以获取一个SDS的长度的复杂度为O(1).

Redis使用SDS, 将获取字符串长度所需的复杂度从O(N)降低到O(1), 确保获取字符串长度的工作不会成为Redis的性能瓶颈.

2.2 杜绝缓冲区溢出

由于C字符串不记录自身长度, 以函数strcat来说, 当执行该函数时, 都是认为已经为dest分配了足够的内存容纳src字符串, 但如果该假设不成立, 就会产生缓冲区溢出.

然而, SDS的字符串空间分配策略, 从根本上杜绝了缓冲区溢出的可能性: 当SDS-API需要对SDS进行修改时, API会先检查SDS的空间是否满足修改所需的需求, 如果不满足的话, API会自动扩容至所需大小, 再执行修改操作. 所以, SDS无需手工维护SDS的空间大小, 也不会产生缓冲区溢出的问题.

2.3 减少修改字符串时带来的内存重分配次数

由于C字符串不记录自身长度, 所以每次增长或缩减字符串, 需要对保存这个C字符串的数组进行一次内存重分配操作:

1.如果程序执行的是增长字符串的操作, 比如拼接操作append, 需要进行内存重分配操作, 扩展底层数组至合适大小, 否则将会产生缓冲区溢出;
2.如果程序执行的是缩短字符串的操作, 比如截断操作trim, 需要进行内存释放操作, 否则将会产生内存泄露.

Redis作为数据库, 经常用于速度要求严苛, 数据被频繁修改的场合, 减少内存的重分配次数能提高性能. SDS通过未使用空间解除了字符串长度底层数组长度之间的关联: 在SDS中, buf数组的长度不一定就是字符数加一, 数组里面可以包含未使用的字节, 而这些字节的数量就由SDS的free属性记录.

通过未使用空间, SDS实现了空间预分配和惰性空间释放两种优化策略.

1.空间预分配

当SDS-API对SDS进行修改, 并且需要对SDS进行空间扩展的时候, 程序不仅会为SDS分配修改所需的空间, 还会为SDS分配额外的未使用空间.

额外分配的未使用空间数量, 由以下公式决定:

  1. 如果对SDS进行修改之后, SDS的长度(即len属性)将小于1MB, 那么程序分配和len属性同样大小的未使用空间, 这时SDS的len属性的值将和free属性的值相同: 比如修改之后, SDS的len将变成13字节, 那么程序也会分配13字节的未使用空间, buf长度将变成 13Byte + 13Byte + 1Byte = 27Byte;
  2. 如果对SDS进行修改之后, SDS的长度将大于等于1MB, 那么程序会分配1MB的未使用空间: 比如修改之后, SDS的len将变成30MB, 那么程序会分配1MB的未使用空间, buf长度将变成 30MB + 1MB + 1Byte.

通过空间预分配策略, Redis可以减少连续执行字符串增长操作所需的内存重分配次数.

2.惰性空间释放

惰性空间释放用于优化SDS的字符串缩短操作: 当SDS-API需要缩短SDS字符串时, 程序并不会立即回收内存, 而是使用free属性将这些字节的数量记录起来, 并等待将来使用.

当然, SDS也提供了相应的API真正地释放SDS的未使用空间, 无需担心该策略带来的内存浪费问题.

2.4 二进制安全

C字符串除了末尾, 不能包含空字符, 否则最先被程序读入的空字符将被误认为是字符串结尾, 所以C字符串只能保存文本数据, 而不能保存像图片, 音频, 视频, 压缩文件这样的二进制数据.

Redis为了可以适用于各种不同的场景, SDS-API都是二进制安全的(binary-safe), 所有SDS-API都会以二进制的方式处理buf数组里面的数据, 不限制, 不过滤, 不假设, 写入什么就读到什么.

2.5 兼容部分C字符串函数

虽然SDS-API是二进制安全的, 但它们一样遵循C字符串以空字符结尾的惯例: 这些API总是将SDS保存的数据末尾数组为空字符, 并且总会在为buf分配空间时多分配一个字节来容纳这个空字符, 以复用<string.h>库定义的函数.

3. SDS的API

函数 作用 复杂度
sdsnew 创建一个包含给定C字符串的SDS O(N), N为给定C字符串的长度
sdsempty 创建一个不包含任何内容的空SDS O(1)
sdsfree 释放给定的SDS O(N), N为被释放SDS的长度
sdslen 返回SDS的已使用空间字节数 这个值可以通过读取SDS的len属性来直接获得, 复杂度为O(1)
sdsavail 返回SDS的未使用空间字节数 这个值可以通过读取SDS的free属性来直接获得, 复杂度为O(1)
sdsdup 创建一个给定SDS的副本(copy) O(N), N为给定C字符串的长度
sdsclear 清空SDS保存的字符串内容 因为惰性空间释放策略, 复杂度为O(1)
sdscat 将给定C字符串拼接到另一个SDS字符串的末尾 O(N), N为被拼接C字符串的长度
sdscatsds 将给定SDS字符串拼接到另一个SDS字符串的末尾 O(N), N为被拼接SDS字符串的长度
sdscpy 将给定的C字符串复制到SDS里面, 覆盖SDS原有的字符串 O(N), N为被复制SDS字符串的长度
sdsgrowzero 用空字符将SDS扩展至给定长度 O(N), N为扩展新增的字节数
sdsrange 保留SDS给定区间内的数据, 不在区间内的数据会被覆盖或清除 O(N), N为被保留数据的字节数
sdstrim 接受一个SDS和一个C字符串作为参数, 从SDS中移除所有在C字符串中出现过的字符 O(N^2), N为给定C字符串的长度
sdscmp 对比两个SDS字符串是否相同 O(N), N为两个SDS钟较短的那个SDS的长度

4. 总结

  • Redis在大多数情况下使用SDS作为字符串的表示;
  • 相比C字符串, SDS具有以下优点:
  1. 常熟复杂度获取字符串长度;
  2. 杜绝缓冲区溢出;
  3. 减少修改字符串长度时所需的内存重分配次数;
  4. 二进制安全;
  5. 兼容部分C字符串函数.
  • C字符串和SDS之间的区别:
C字符串SDS
获取字符串长度的复杂度为O(N)获取字符串长度的复杂度为O(1)
API是不安全的,可能造成缓冲区溢出API是安全的,不会造成缓冲区溢出
修改字符串长度N次必然需要执行N次内存重分配修改字符串长度N次最多需啊哟执行N次内存重分配
可以使用所有<string.h>库中的函数可以使用一部分<string.h>库中的函数

以上笔记都是整理自<Redis的设计与实现>, 真的很感谢作者, 也希望自己能坚持写下去^_^.

来, 先看效果哈哈哈哈!

演示地址: 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/

演示地址: 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点! 睡觉睡觉! 晚安世界!

相关文章

上一篇文章 提到的哈希值, 将会在这篇文章揭晓, 看完这篇文章, 也将会清楚地了解 Composer 镜像的工作原理.

认识 Composer 镜像的工作原理之前, 先来认识一个非常好用的参数 -vvv:

composer require monolog/monolog -vvv

随便找个目录, 执行上面的命令, 会看到这些东西:

Downloading https://packagist.phpcomposer.com/packages.json
Writing C:/Users/Administrator/AppData/Local/Composer/repo/https---packagist.phpcomposer.com/packages.json into cache
Downloading https://packagist.phpcomposer.com/p/provider-2013%2427453ee820a0c569990f0e5c705651d4902266bfb0d66ac4a8675350cc8c3dee.json
Writing C:/Users/Administrator/AppData/Local/Composer/repo/https---packagist.phpcomposer.com/p-provider-2013.json into cache

OK, 这个链接 https://packagist.phpcomposer.com/packages.json 是我们关注的重点, 看看里面有什么:

{
    "packages":[

    ],
    "notify":"https://packagist.org/downloads/%package%",
    "notify-batch":"https://packagist.org/downloads/",
    "providers-url":"/p/%package%$%hash%.json",
    "search":"https://packagist.org/search.json?q=%query%&type=%type%",
    "provider-includes":{
        "p/provider-2013$%hash%.json":{
            "sha256":"27453ee820a0c569990f0e5c705651d4902266bfb0d66ac4a8675350cc8c3dee"
        },
        "p/provider-2014$%hash%.json":{
            "sha256":"54926c64d3af83e29338c05bf2e0b4273786b644493976b91ac521e48fcb0898"
        },
        "p/provider-2015$%hash%.json":{
            "sha256":"3bd588c60bfd7845a93af3f834dd2f45b975cd70a8cb8d4ea3b1dd40c9859454"
        },
        "p/provider-2016$%hash%.json":{
            "sha256":"430744185fc781f75b4e9c966b3954f374c24f200d8124a5a43b15bce115aded"
        },
        "p/provider-2017-01$%hash%.json":{
            "sha256":"443ade7677cb86f103d95725a043725bfeb6f80c65a7f28b7295afd2874642c6"
        },
        "p/provider-2017-04$%hash%.json":{
            "sha256":"9343747a3c94f2c54b8a0ac2b7df5fd01f6b38e377c21a10ea4af19dd7739088"
        },
        "p/provider-2017-07$%hash%.json":{
            "sha256":"5c17b5bf4ace4c44112b4e3db1dbc7efd891bd023e329afdb346753fd388596c"
        },
        "p/provider-2017-10$%hash%.json":{
            "sha256":"b04c29739124e5da0f39c48683f7ddcf2cc04483e6022967d0f5f3b8dc662ae2"
        },
        "p/provider-archived$%hash%.json":{
            "sha256":"97ce4ddebac5598dcb1c82c70de4254889755951bf988c34415fddfa09ad83b4"
        },
        "p/provider-latest$%hash%.json":{
            "sha256":"54d70158a7603fb1d47e64d1a11a4518244a4752039ae6d8dd1e9e3c07bf5665"
        }
    },
    "sync-time":"2017-12-02T03:37:21+00:00",
    "how-to-use-this-packagist-mirror":"https://pkg.phpcomposer.com/",
    "total-cached-packages":177423,
    "total-cached-zips":"Millions"
}

/packages.json 我们称它为 入口配置 吧, 里面有一个字段 providers-includes, 我们再回头看看这个(注意%24解码后是$):

Downloading https://packagist.phpcomposer.com/p/provider-2013%2427453ee820a0c569990f0e5c705651d4902266bfb0d66ac4a8675350cc8c3dee.json

是不是很神奇, 把里面的 sha256 替换到 %hash%, 再加上域名, 就是一个下载链接.

我们把它下载到本地, 发现里面是这样的结构(由于文件太大, 所以我删掉了很多包, 理解它结构就可以了):

{
    "providers":{
        "2085020/api_pingdom":{
            "sha256":"285e015fe97e2fa0b235f7052a9b642e41ae669c6c4b17116ae6585ebcfafa9d"
        },
        "3rd-party/smarty":{
            "sha256":"c7bd2d6b6927649c4c200f5ff9d65cadb32af2fe3330cd698a25930224337c1a"
        },
        "3rd-party/yaml":{
            "sha256":"d94f101c9c348f369890443aa5e6d4d5e6d911b7ecc4edcbcbc12fcb6494ed08"
        },

        "zucchi/pages":{
            "sha256":"40cb6ea861cc93f26ed6d6de6b284e76f7aadef690e3b14529538f5af82b33f0"
        }
    }
}

可以看到, 里面有个 providers 字段, 字段里面有很多很多个包(我拿了前三个和最后一个), 每个包都带有一个哈希值.

OK, 答案就在这里, 上一篇文章苦苦追寻的哈希值, 其实就是在保存在这些文件里面, 它的作用大家也应该猜到了, 其实就是校验而已, 也可以说是一个版本号的功能, 当包信息有更新时, 哈希会被更新, 旧的哈希随之失效.

provider-includes 里面的 JSON, 可以理解为哈希分片信息. 从 total-cached-packages 可以知道, 一共有 177423 个包, 如果这么多的包的哈希信息保存在一个 JSON 文件当中, PHP 加载起来会很吃力, 拆成多个分片, 如果在某个分片中找到哈希, 其他分片就不需要再找了. 我实验过, 一个包只能在一个分片中出现.


总结, 一个 Composer 镜像服务器, 由 入口配置, 组件哈希, composer.json 这三部分组成. 拆开看完发现蛮简单的.

相关文章

相关文章

有使用PHP组件的朋友, 应该会注意到组件里头会有一个文件 composer.json, 它描述了组件的信息: 名称, 描述, 关键词, 作者, GitHub仓库地址...还有它所依赖的子组件, 是 Composer 工作的核心.

拿一个大家都知道的日志组件 monologcomposer.json 为例, 我说下一些比较重要的字段:

{
    "name": "monolog/monolog",
    "description": "Sends your logs to files, sockets, inboxes, databases and various web services",
    "keywords": ["log", "logging", "psr-3"],
    "homepage": "http://github.com/Seldaek/monolog",
    "type": "library",
    "license": "MIT",
    "authors": [
        {
            "name": "Jordi Boggiano",
            "email": "j.boggiano@seld.be",
            "homepage": "http://seld.be"
        }
    ],
    "require": {
        "php": "^7.0",
        "psr/log": "^1.0.1"
    },
    "require-dev": {
        "phpunit/phpunit": "^5.7",
        "graylog2/gelf-php": "^1.4.2",
        "sentry/sentry": "^0.13",
        "ruflin/elastica": ">=0.90 <3.0",
        "doctrine/couchdb": "~1.0@dev",
        "aws/aws-sdk-php": "^2.4.9 || ^3.0",
        "php-amqplib/php-amqplib": "~2.4",
        "swiftmailer/swiftmailer": "^5.3|^6.0",
        "php-console/php-console": "^3.1.3",
        "jakub-onderka/php-parallel-lint": "^0.9",
        "predis/predis": "^1.1",
        "phpspec/prophecy": "^1.6.1"
    },
    "suggest": {
        "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
        "sentry/sentry": "Allow sending log messages to a Sentry server",
        "doctrine/couchdb": "Allow sending log messages to a CouchDB server",
        "ruflin/elastica": "Allow sending log messages to an Elastic Search server",
        "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
        "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
        "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)",
        "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)",
        "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
        "rollbar/rollbar": "Allow sending log messages to Rollbar",
        "php-console/php-console": "Allow sending log messages to Google Chrome"
    },
    "autoload": {
        "psr-4": {"Monolog\\": "src/Monolog"}
    },
    "autoload-dev": {
        "psr-4": {"Monolog\\": "tests/Monolog"}
    },
    "provide": {
        "psr/log-implementation": "1.0.0"
    },
    "extra": {
        "branch-alias": {
            "dev-master": "2.0.x-dev"
        }
    },
    "scripts": {
        "test": [
            "parallel-lint . --exclude vendor",
            "phpunit"
        ]
    }
}

安装依赖的时候, 最重要的字段是 name, require 以及 require-dev.

name

该字段标识了组件的名称, 在 所有 的组件中, 它是唯一的;

require

该字段列举出该组件 所需的运行环境 以及 依赖的子组件的版本, 安装该组件时, 会检测运行环境, 并安装该组件的子组件, 以及这些子组件的所有子组件...直到子组件不再依赖任何组件为止;

require-dev

该字段不是必须的, 一般来说不安装里面的依赖, 也是可以用的, 通常都是用来跑单元测试. 依赖的安装同 require 字段.

其他字段对于理解镜像的原理没什么帮助, 有兴趣可以看下 这篇文章.

安装依赖的过程, 其实就是请求服务器, 要求拿到该组件的 composer.json 文件, 然后 JSON 解析, 得到 requirerequire-dev 字段的组件, 一直遍历下去, 根据文件描述的仓库地址 git clone 到本地.

看过我 上一篇文章 的朋友就知道, 文章末尾我们配置了 国内的composer镜像, 用来加速我们安装组件的过程, 它缓存了所有包的 composer.json, 并把仓库的每一个分支源码, 打包为 zip 压缩包, 并结合 cdn 加速.

镜像服务器提供了让我们得到 composer.json 的接口, 我们只需提交一个包名, 还有请求结果的哈希值(是不是很懵逼, 我怎么知道结果的哈希值), 镜像服务器会返回一个 JSON, 它包含了很多 composer.json (至少一个), 这些 composer.json 里面就有我们要找的组件的 composer.json (根据name字段), 也包括了其他包的, 为什么会带有其他包的呢, 我捣鼓了挺多次, 发现是当 require 字段存在时, 它就顺带返回了, 不过也不是绝对的, 可能考虑体积关系, 也不会返回太多.

说了这么多, 看下 psr/log 组件的请求结果吧, 比较长, 它包含了5个包的信息:

hackification/log
mobio/target
notadd/wechat
psr/log
wedeto/log
{
    "packages":{
        "hackification/log":{
            "1.0.2":{
                "name":"hackification/log",
                "description":"Common interface for logging libraries",
                "keywords":[
                    "log",
                    "psr",
                    "psr-3",
                    "hack",
                    "hacklang"
                ],
                "homepage":"https://github.com/hackification/log",
                "version":"1.0.2",
                "version_normalized":"1.0.2.0",
                "license":[
                    "MIT"
                ],
                "authors":[
                    {
                        "name":"PHP-FIG",
                        "homepage":"http://www.php-fig.org/"
                    }
                ],
                "source":{
                    "type":"git",
                    "url":"https://github.com/hackification/log.git",
                    "reference":"75b02e14bd8e5b8ded75de91d7eb9df6046f7fa7"
                },
                "dist":{
                    "type":"zip",
                    "url":"https://files.phpcomposer.com/files/hackification/log/75b02e14bd8e5b8ded75de91d7eb9df6046f7fa7.zip",
                    "reference":"75b02e14bd8e5b8ded75de91d7eb9df6046f7fa7",
                    "shasum":""
                },
                "type":"library",
                "time":"2016-11-08T15:32:34+00:00",
                "autoload":{
                    "psr-4":{
                        "Psr\Log\":"Psr/Log/"
                    }
                },
                "extra":{
                    "branch-alias":{
                        "dev-master":"1.0.x-dev"
                    }
                },
                "require":{
                    "hhvm":">=3.0.0"
                },
                "replace":{
                    "psr/log":"*"
                },
                "uid":1072563
            },
            "dev-master":{
                "name":"hackification/log",
                "description":"Common interface for logging libraries",
                "keywords":[
                    "log",
                    "psr",
                    "psr-3",
                    "hack",
                    "hacklang"
                ],
                "homepage":"https://github.com/hackification/log",
                "version":"dev-master",
                "version_normalized":"9999999-dev",
                "license":[
                    "MIT"
                ],
                "authors":[
                    {
                        "name":"PHP-FIG",
                        "homepage":"http://www.php-fig.org/"
                    }
                ],
                "source":{
                    "type":"git",
                    "url":"https://github.com/hackification/log.git",
                    "reference":"75b02e14bd8e5b8ded75de91d7eb9df6046f7fa7"
                },
                "dist":{
                    "type":"zip",
                    "url":"https://files.phpcomposer.com/files/hackification/log/75b02e14bd8e5b8ded75de91d7eb9df6046f7fa7.zip",
                    "reference":"75b02e14bd8e5b8ded75de91d7eb9df6046f7fa7",
                    "shasum":""
                },
                "type":"library",
                "time":"2016-11-08T15:32:34+00:00",
                "autoload":{
                    "psr-4":{
                        "Psr\Log\":"Psr/Log/"
                    }
                },
                "extra":{
                    "branch-alias":{
                        "dev-master":"1.0.x-dev"
                    }
                },
                "require":{
                    "hhvm":">=3.0.0"
                },
                "replace":{
                    "psr/log":"*"
                },
                "uid":1072564
            }
        },
        "mobio/target":{
            "0.0.5":{
                "name":"mobio/target",
                "description":"PHP library for myTarget API",
                "keywords":[
                    "php",
                    "myTarget"
                ],
                "homepage":"",
                "version":"0.0.5",
                "version_normalized":"0.0.5.0",
                "license":[
                    "MIT"
                ],
                "authors":[

                ],
                "source":{
                    "type":"git",
                    "url":"https://github.com/MobioInc/target.git",
                    "reference":"5baeaae1aa7d85c5b5fd4e33a06608a1de93d73b"
                },
                "dist":{
                    "type":"zip",
                    "url":"https://files.phpcomposer.com/files/MobioInc/target/5baeaae1aa7d85c5b5fd4e33a06608a1de93d73b.zip",
                    "reference":"5baeaae1aa7d85c5b5fd4e33a06608a1de93d73b",
                    "shasum":""
                },
                "type":"library",
                "time":"2016-10-31T08:52:52+00:00",
                "autoload":{
                    "psr-4":{
                        "Mobio\Target\":"src/"
                    }
                },
                "require":{
                    "php":">=5.5.0",
                    "psr/log":"~1.0",
                    "guzzlehttp/guzzle":"^6.1"
                },
                "require-dev":{
                    "phpunit/phpunit":"^5.5"
                },
                "provide":{
                    "psr/log":"1.0.0"
                },
                "uid":1060012
            },
            "dev-master":{
                "name":"mobio/target",
                "description":"PHP library for myTarget API",
                "keywords":[
                    "php",
                    "myTarget"
                ],
                "homepage":"",
                "version":"dev-master",
                "version_normalized":"9999999-dev",
                "license":[
                    "MIT"
                ],
                "authors":[

                ],
                "source":{
                    "type":"git",
                    "url":"https://github.com/MobioInc/target.git",
                    "reference":"70aa382ca6d3ba3b5a834bbe85d3fc2cbfec965f"
                },
                "dist":{
                    "type":"zip",
                    "url":"https://files.phpcomposer.com/files/MobioInc/target/70aa382ca6d3ba3b5a834bbe85d3fc2cbfec965f.zip",
                    "reference":"70aa382ca6d3ba3b5a834bbe85d3fc2cbfec965f",
                    "shasum":""
                },
                "type":"library",
                "time":"2017-02-13T18:53:10+00:00",
                "autoload":{
                    "psr-4":{
                        "Mobio\Target\":"src/"
                    }
                },
                "require":{
                    "php":">=5.5.0",
                    "guzzlehttp/guzzle":"^6.1",
                    "psr/log":"~1.0"
                },
                "require-dev":{
                    "phpunit/phpunit":"^5.5"
                },
                "provide":{
                    "psr/log":"1.0.0"
                },
                "uid":700331
            }
        },
        "notadd/wechat":{
            "dev-master":{
                "name":"notadd/wechat",
                "description":"Notadd's Wechat Module.",
                "keywords":[
                    "framework",
                    "cms",
                    "member",
                    "notadd"
                ],
                "homepage":"https://notadd.com",
                "version":"dev-master",
                "version_normalized":"9999999-dev",
                "license":[
                    "Apache-2.0"
                ],
                "authors":[
                    {
                        "name":"Notadd",
                        "email":"notadd@ibenchu.com"
                    }
                ],
                "source":{
                    "type":"git",
                    "url":"https://github.com/notadd/wechat.git",
                    "reference":"e3f684cd225f3fadf21953c0289cb8426baad0e5"
                },
                "dist":{
                    "type":"zip",
                    "url":"https://files.phpcomposer.com/files/notadd/wechat/e3f684cd225f3fadf21953c0289cb8426baad0e5.zip",
                    "reference":"e3f684cd225f3fadf21953c0289cb8426baad0e5",
                    "shasum":""
                },
                "type":"notadd-module",
                "time":"2017-11-13T04:23:05+00:00",
                "autoload":{
                    "psr-4":{
                        "Notadd\Wechat\":"src/"
                    }
                },
                "require":{
                    "php":">=7.0",
                    "overtrue/wechat":"~3.1"
                },
                "require-dev":{
                    "notadd/installers":"0.14.*",
                    "notadd/testing":"0.4.*",
                    "phpunit/phpunit":"~6.0"
                },
                "replace":{
                    "guzzlehttp/guzzle":"*",
                    "guzzlehttp/promises":"*",
                    "guzzlehttp/psr7":"*",
                    "monolog/monolog":"*",
                    "psr/container":"*",
                    "psr/http-message":"*",
                    "psr/log":"*",
                    "symfony/http-foundation":"*",
                    "symfony/polyfill-mbstring":"*",
                    "symfony/psr-http-message-bridge":"*"
                },
                "uid":1108963
            }
        },
        "psr/log":{
            "1.0.0":{
                "name":"psr/log",
                "description":"Common interface for logging libraries",
                "keywords":[
                    "log",
                    "psr",
                    "psr-3"
                ],
                "homepage":"",
                "version":"1.0.0",
                "version_normalized":"1.0.0.0",
                "license":[
                    "MIT"
                ],
                "authors":[
                    {
                        "name":"PHP-FIG",
                        "homepage":"http://www.php-fig.org/"
                    }
                ],
                "source":{
                    "type":"git",
                    "url":"https://github.com/php-fig/log.git",
                    "reference":"fe0936ee26643249e916849d48e3a51d5f5e278b"
                },
                "dist":{
                    "type":"zip",
                    "url":"https://files.phpcomposer.com/files/php-fig/log/fe0936ee26643249e916849d48e3a51d5f5e278b.zip",
                    "reference":"fe0936ee26643249e916849d48e3a51d5f5e278b",
                    "shasum":""
                },
                "type":"library",
                "time":"2012-12-21T11:40:51+00:00",
                "autoload":{
                    "psr-0":{
                        "Psr\Log\":""
                    }
                },
                "uid":29358
            },
            "1.0.1":{
                "name":"psr/log",
                "description":"Common interface for logging libraries",
                "keywords":[
                    "log",
                    "psr",
                    "psr-3"
                ],
                "homepage":"https://github.com/php-fig/log",
                "version":"1.0.1",
                "version_normalized":"1.0.1.0",
                "license":[
                    "MIT"
                ],
                "authors":[
                    {
                        "name":"PHP-FIG",
                        "homepage":"http://www.php-fig.org/"
                    }
                ],
                "source":{
                    "type":"git",
                    "url":"https://github.com/php-fig/log.git",
                    "reference":"5277094ed527a1c4477177d102fe4c53551953e0"
                },
                "dist":{
                    "type":"zip",
                    "url":"https://files.phpcomposer.com/files/php-fig/log/5277094ed527a1c4477177d102fe4c53551953e0.zip",
                    "reference":"5277094ed527a1c4477177d102fe4c53551953e0",
                    "shasum":""
                },
                "type":"library",
                "time":"2016-09-19T16:02:08+00:00",
                "autoload":{
                    "psr-4":{
                        "Psr\Log\":"Psr/Log/"
                    }
                },
                "extra":{
                    "branch-alias":{
                        "dev-master":"1.0.x-dev"
                    }
                },
                "require":{
                    "php":">=5.3.0"
                },
                "uid":1000789
            },
            "1.0.2":{
                "name":"psr/log",
                "description":"Common interface for logging libraries",
                "keywords":[
                    "log",
                    "psr",
                    "psr-3"
                ],
                "homepage":"https://github.com/php-fig/log",
                "version":"1.0.2",
                "version_normalized":"1.0.2.0",
                "license":[
                    "MIT"
                ],
                "authors":[
                    {
                        "name":"PHP-FIG",
                        "homepage":"http://www.php-fig.org/"
                    }
                ],
                "source":{
                    "type":"git",
                    "url":"https://github.com/php-fig/log.git",
                    "reference":"4ebe3a8bf773a19edfe0a84b6585ba3d401b724d"
                },
                "dist":{
                    "type":"zip",
                    "url":"https://files.phpcomposer.com/files/php-fig/log/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d.zip",
                    "reference":"4ebe3a8bf773a19edfe0a84b6585ba3d401b724d",
                    "shasum":""
                },
                "type":"library",
                "time":"2016-10-10T12:19:37+00:00",
                "autoload":{
                    "psr-4":{
                        "Psr\Log\":"Psr/Log/"
                    }
                },
                "extra":{
                    "branch-alias":{
                        "dev-master":"1.0.x-dev"
                    }
                },
                "require":{
                    "php":">=5.3.0"
                },
                "uid":1029935
            },
            "dev-master":{
                "name":"psr/log",
                "description":"Common interface for logging libraries",
                "keywords":[
                    "log",
                    "psr",
                    "psr-3"
                ],
                "homepage":"https://github.com/php-fig/log",
                "version":"dev-master",
                "version_normalized":"9999999-dev",
                "license":[
                    "MIT"
                ],
                "authors":[
                    {
                        "name":"PHP-FIG",
                        "homepage":"http://www.php-fig.org/"
                    }
                ],
                "source":{
                    "type":"git",
                    "url":"https://github.com/php-fig/log.git",
                    "reference":"4ebe3a8bf773a19edfe0a84b6585ba3d401b724d"
                },
                "dist":{
                    "type":"zip",
                    "url":"https://files.phpcomposer.com/files/php-fig/log/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d.zip",
                    "reference":"4ebe3a8bf773a19edfe0a84b6585ba3d401b724d",
                    "shasum":""
                },
                "type":"library",
                "time":"2016-10-10T12:19:37+00:00",
                "autoload":{
                    "psr-4":{
                        "Psr\Log\":"Psr/Log/"
                    }
                },
                "extra":{
                    "branch-alias":{
                        "dev-master":"1.0.x-dev"
                    }
                },
                "require":{
                    "php":">=5.3.0"
                },
                "uid":29285
            }
        },
        "wedeto/log":{
            "v0.9.1":{
                "name":"wedeto/log",
                "description":"Wedeto Platform - Logger",
                "keywords":[

                ],
                "homepage":"https://wedeto.net/",
                "version":"v0.9.1",
                "version_normalized":"0.9.1.0",
                "license":[
                    "MIT"
                ],
                "authors":[
                    {
                        "name":"Egbert van der Wal",
                        "email":"ewal@pointpro.nl"
                    }
                ],
                "source":{
                    "type":"git",
                    "url":"https://github.com/Wedeto/Log.git",
                    "reference":"acc7f4aa66965dd2627a3886c9ac8b0d77b0a268"
                },
                "dist":{
                    "type":"zip",
                    "url":"https://files.phpcomposer.com/files/Wedeto/Log/acc7f4aa66965dd2627a3886c9ac8b0d77b0a268.zip",
                    "reference":"acc7f4aa66965dd2627a3886c9ac8b0d77b0a268",
                    "shasum":""
                },
                "type":"library",
                "time":"2017-04-10T12:31:55+00:00",
                "autoload":{
                    "psr-4":{
                        "Wedeto\Log\":"src/"
                    }
                },
                "require":{
                    "psr/log":"1.0.*",
                    "wedeto/util":"0.9.*",
                    "php":">=7.0.0"
                },
                "require-dev":{
                    "mikey179/vfsstream":"~1"
                },
                "provide":{
                    "psr/log":"1.0.*"
                },
                "uid":1336127
            },
            "v0.9.2":{
                "name":"wedeto/log",
                "description":"Wedeto Platform - Logger",
                "keywords":[

                ],
                "homepage":"https://wedeto.net/",
                "version":"v0.9.2",
                "version_normalized":"0.9.2.0",
                "license":[
                    "MIT"
                ],
                "authors":[
                    {
                        "name":"Egbert van der Wal",
                        "email":"ewal@pointpro.nl"
                    }
                ],
                "source":{
                    "type":"git",
                    "url":"https://github.com/Wedeto/Log.git",
                    "reference":"55e6b03f0b446b7054078fd8a680ad1499d26264"
                },
                "dist":{
                    "type":"zip",
                    "url":"https://files.phpcomposer.com/files/Wedeto/Log/55e6b03f0b446b7054078fd8a680ad1499d26264.zip",
                    "reference":"55e6b03f0b446b7054078fd8a680ad1499d26264",
                    "shasum":""
                },
                "type":"library",
                "time":"2017-04-10T18:15:36+00:00",
                "autoload":{
                    "psr-4":{
                        "Wedeto\Log\":"src/"
                    }
                },
                "require":{
                    "psr/log":"1.0.*",
                    "wedeto/util":"0.9.*",
                    "php":">=7.0.0"
                },
                "require-dev":{
                    "mikey179/vfsstream":"~1"
                },
                "provide":{
                    "psr/log":"1.0.*"
                },
                "uid":1336697
            }
        }
    }
}

可以看到, packages 字段里面有5个包, 里面的 psr/log 字段就是我们要找的, 而里面有各个分支的 composer.json, 以分支 1.0.0 为例, 里面有两个很关键的字段, sourcedist:

{
    "source":{
        "type":"git",
        "url":"https://github.com/php-fig/log.git",
        "reference":"fe0936ee26643249e916849d48e3a51d5f5e278b"
    },
    "dist":{
        "type":"zip",
        "url":"https://files.phpcomposer.com/files/php-fig/log/fe0936ee26643249e916849d48e3a51d5f5e278b.zip",
        "reference":"fe0936ee26643249e916849d48e3a51d5f5e278b",
        "shasum":""
    }
}

dist

该字段其实就是加速的 zip 压缩包, 无需 git clone, 只需把 zip 下载到本地, 解压完, 分支 1.0.0 就装好了.

source

这个字段的作用, 就是万一 dist 字段的 zip 下载不了, 不会马上中断整个安装流程, 而是接着 git clone. 也就是说, dist 字段失败, 或者压根就没有 dist 字段, 就走 source 字段.


看到这里, 对 Composer 的了解应该多了很多吧? 还记得 请求结果的哈希值 吗? 这个哈希哪里来的, 为什么我可以提前知道这个请求的 JSON 的哈希值? 还有, 接口在哪里? 镜像服务器的官方网站, 并没有提供啊...

下一篇文章再告诉你.

相关文章