刘泉皓

没有最强的法术,只有最强的法师。

对QRCode的“Code Length OverFlow Error”问题详细分析和解决方法

21 Oct 2018 » memory

我的博客分享功能使用的是share.js这个项目,它的微信分享功能使用的是QRCodejs这个项目。然后就在昨天发表了《Centos7从初次登陆服务器到安装nginx配置https全解》后,噩梦开始了。。

一、发现错误应急调试

昨天兴高采烈的写完《Centos7从初次登陆服务器到安装nginx配置https全解》后,和平时一样,使用了bundle exec jekyll buildmarkdown写的博客编译成了静态网页,一切正常。然后我用rsync把生成好的网页同步到了服务器上。和平时一样,上传完博客后我就用Chrome浏览器打开网站欣赏一番,然后f12打开调试工具,按下f5刷新一下,愉悦的感受一下博客的网速。然而就在网页刷新那一刻,Console的报错打破了我平静的生活:

img1

虽然那一瞬间心中一阵诧异,但马上回过神来。“这是在羞辱老夫”,我不禁喃喃自语。毕竟我纵横互联网五百年,什么风浪没见过。一眼瞟去错误信息,发现是social-share.min.js抛出了异常。“应该是分享插件出了问题”,心中一瞬间定位出了方向,然后我把页面拖到最下面看看分享按钮:

img2

“靠,什么时候出的问题”,我不经骂道。于是我赶紧点开了前几次写的博客:

img3

“嘶~没问题啊”,我又回到出问题的博客,刷新几下依然有问题:

此时的我,一阵疑惑。但作为有多年开发的我,经验毕竟是在这,就像多年的战士一般,单刀赴会,直奔主题。

我看了一下抛出的异常栈,先看看最近的地方at Function.e.createData (social-share.min.js:1),于是我直接拿createData去未压缩的源码搜索,发现都是在qrcode.js里,方法是QRCodeModel.createData,因为对象名压缩后QRCodeModel会变成很短的字符,看看压缩文件前后代码可以分析出来位置。其实也就有两处调用而已,我马上就找到抛出异常的位置了:

img4

“既然是QRCodeModel的方法,那就再看看它是什么吧”,我这么想到,于是我又开始搜索它,虽然有些多,但也马上找到一些蛛丝马迹了:

img5

QRCodemakeCode方法里新建了一个它的对象,QRCodemakeCode方法是熟悉的,它就在前面浏览器抛出的异常调用栈里。再看看QRCode是哪里用的,很快我也找到位置了,在social-share.js文件里:

img6

看看这里的代码,发现有wechat字样,div标签里也有wechat-qrcode,大致明白了是微信分享的二维码出了问题,异常信息就是“代码长度溢出”,说明在生成分享二维码的时候,输入给QRCode的内容太多导致抛出异常。先不管那么多,让线上正常再说,把微信分享先去掉:

      <!-- share -->
      <div class="footer-col footer-col-3 social-share"></div>
      <link rel="stylesheet" href="/assets/css/share.min.css">
      <script src="/assets/js/social-share.min.js"></script>
      <script>
        var $config = {
          //sites : ['weibo', 'wechat', 'qq', 'qzone', 'twitter', 'google'],
          sites : ['weibo', 'qq', 'qzone', 'twitter', 'google'],
        };
         socialShare('.social-share', $config);
      </script>
      <!-- share end -->

已经没有报错了:

img7

微信分享被去掉了:

img8

嗯,一切都正常,赶紧编译上传到服务器里,然后刷新一下CDN缓存。昨天就惊心动魄就此结束了。

二、分析问题

昨天是治标没治本,毕竟微信分享功能没了,不爽。本着较真的性格,今天就来彻底解决问题。其实也没有什么其他方法,这个毕竟是开源项目,既然我遇到了,其他人也应该会遇到,所以用Google总能搜索到什么。于是我拿异常信息去搜索,果然是找了解决方法。

相关错误讨论在这里,修复此问题的commit这里

看看修复的代码,其实也就是修改了下QRRSBlock.RS_BLOCK_TABLE数组,把[11,36,12]换成了[11,36,12,7,37,13],也就是在原有的数组上增加了些内容,这个数组的意思是:[count, totalCount, dataCount][count, totalCount, dataCount, count, totalCount, dataCount],增加后三位就是增加了大小。再看看抛出异常时的代码:

QRCodeModel.createData = function (typeNumber, errorCorrectLevel, dataList) {
    var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectLevel);
    var buffer = new QRBitBuffer();
    for (var i = 0; i < dataList.length; i++) {
        var data = dataList[i];
        buffer.put(data.mode, 4);
        buffer.put(data.getLength(), QRUtil.getLengthInBits(data.mode, typeNumber));
        data.write(buffer);
    }
    var totalDataCount = 0;
    for (var i = 0; i < rsBlocks.length; i++) {
        totalDataCount += rsBlocks[i].dataCount;
    }
    if (buffer.getLengthInBits() > totalDataCount * 8) {
        throw new Error("code length overflow. ("
            + buffer.getLengthInBits()
            + ">"
            + totalDataCount * 8
            + ")");
    }
...

可以看见,如果buffer.getLengthInBits() > totalDataCount * 8判断成立,就抛出异常,也就是数据是实际大小大于二维码预申请资源缓存的大小就抛出异常。totalDataCount就是rsBlocks数组中的dataCount累加出来的大小。再看看QRRSBlock.getRSBlocks()是什么:

function QRRSBlock(totalCount, dataCount) {
    this.totalCount = totalCount;
    this.dataCount = dataCount;
}
QRRSBlock.getRSBlocks = function (typeNumber, errorCorrectLevel) {
    var rsBlock = QRRSBlock.getRsBlockTable(typeNumber, errorCorrectLevel);
    if (rsBlock == undefined) {
        throw new Error("bad rs block @ typeNumber:" + typeNumber + "/errorCorrectLevel:" + errorCorrectLevel);
    }
    var length = rsBlock.length / 3; var list = [];
    for (var i = 0; i < length; i++) {
        var count = rsBlock[i * 3 + 0];
        var totalCount = rsBlock[i * 3 + 1];
        var dataCount = rsBlock[i * 3 + 2];
        for (var j = 0; j < count; j++) {
            list.push(new QRRSBlock(totalCount, dataCount));
        }
    }
    return list;
}

这里方法里又有一个局部变量rsBlock,从for循环里的操作来看,很明显这个rsBlock是个数组。再看看QRRSBlock.getRsBlockTable()是什么:

var QRErrorCorrectLevel = { L: 1, M: 0, Q: 3, H: 2 }
QRRSBlock.getRsBlockTable = function (typeNumber, errorCorrectLevel) {
    switch (errorCorrectLevel) {
        case QRErrorCorrectLevel.L:
            return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0];
        case QRErrorCorrectLevel.M:
            return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1];
        case QRErrorCorrectLevel.Q:
            return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2];
        case QRErrorCorrectLevel.H:
            return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3];
        default: return undefined;
    }
};

这个方法就是简单的根据errorCorrectLevel的值简单的返回我们修改的数组而已。再看看这个值从哪来:

QRCode = function (el, vOption) {
    this._htOption = {
        width : 256, 
        height : 256,
        typeNumber : 4,
        colorDark : "#000000",
        colorLight : "#ffffff",
        correctLevel : QRErrorCorrectLevel.H
    };
    if (typeof vOption === 'string') {
        vOption	= {
            text : vOption
        };
    }
    // Overwrites options
    if (vOption) {
        for (var i in vOption) {
            this._htOption[i] = vOption[i];
        }
    }
    if (typeof el == "string") {
        el = document.getElementById(el);
    }
    if (this._htOption.useSVG) {
        Drawing = svgDrawer;
    }
    this._android = _getAndroid();
    this._el = el;
    this._oQRCode = null;
    this._oDrawing = new Drawing(this._el, this._htOption);
    if (this._htOption.text) {
        this.makeCode(this._htOption.text);	
    }
}
QRCode.prototype.makeCode = function (sText) {
    this._oQRCode = new QRCodeModel(_getTypeNumber(sText, this._htOption.correctLevel), this._htOption.correctLevel);
    this._oQRCode.addData(sText);
    this._oQRCode.make();
    this._el.title = sText;
    this._oDrawing.draw(this._oQRCode);			
    this.makeImage();
};
function QRCodeModel(typeNumber, errorCorrectLevel) {
    this.typeNumber = typeNumber;
    this.errorCorrectLevel = errorCorrectLevel;
    this.modules = null;
    this.moduleCount = 0;
    this.dataCache = null;
    this.dataList = [];
}
QRCodeModel.prototype = {
    addData: function (data) { var newData = new QR8bitByte(data); this.dataList.push(newData); this.dataCache = null; },
    make: function () { this.makeImpl(false, this.getBestMaskPattern()); }, 
    makeImpl: function (test, maskPattern) {
        ...
        if (this.dataCache == null) { this.dataCache = QRCodeModel.createData(this.typeNumber, this.errorCorrectLevel, this.dataList); }
        this.mapData(this.dataCache, maskPattern);
    }, 
    ...
}

// social-share.js
function createWechat (elem, data) {
    var wechat = getElementsByClassName(elem, 'icon-wechat', 'a');
    if (wechat.length === 0) {
        return false;
    }
    var elems = createElementByString('<div class="wechat-qrcode"><h4>' + data.wechatQrcodeTitle + '</h4><div class="qrcode"></div><div class="help">' + data.wechatQrcodeHelper + '</div></div>');
    var qrcode = getElementsByClassName(elems[0], 'qrcode', 'div');

    new QRCode(qrcode[0], {text: data.url, width: data.wechatQrcodeSize, height: data.wechatQrcodeSize});
    wechat[0].appendChild(elems[0]);
}

从上面代码从内部往上剖析找到数据,然后再从外往内解释就很清楚了。

  1. 首先如果我们打开了微信分享,socialShare会调用createWechat()生成二维码,createWechat()内部会新建QRCode对象。
  2. 新建的QRCode对象会初始化属性this._htOption,this._htOption包含correctLevel : QRErrorCorrectLevel.H属性,并且调用this.makeCode()方法。
  3. makeCode()方法内部会新建QRCodeModel对象,并把上层的correctLevel属性传递进去this._oQRCode = new QRCodeModel(_getTypeNumber(sText, this._htOption.correctLevel), this._htOption.correctLevel);this._htOption.correctLevel也就是QRErrorCorrectLevel.H
  4. function QRCodeModel(typeNumber, errorCorrectLevel)的初始化方法中,会把上层的this._htOption.correctLevel赋值给方法参数errorCorrectLevel,然后在内部赋值给自己的属性this.errorCorrectLevel = errorCorrectLevel;,这时,this.errorCorrectLevel也就是QRErrorCorrectLevel.H了。
  5. makeCode()方法新建QRCodeModel对象后,会再调用QRCodeModel对象的make()方法,make()方法进而会调用makeImpl()方法,而makeImpl()方法会调用QRCodeModel.createData()方法,并把this.errorCorrectLevel属性传递给createData(),此时QRErrorCorrectLevel.H就传递进入了createData()方法中,createData()的方法定义:QRCodeModel.createData = function (typeNumber, errorCorrectLevel, dataList) {...}
  6. createData()方法中,会调用QRRSBlock.getRSBlocks()方法,并把errorCorrectLevel参数传递进去。
  7. getRSBlocks()方法又会调用QRRSBlock.getRsBlockTable(),并把errorCorrectLevel参数传递进去。
  8. 此时,QRErrorCorrectLevel.H就传递给了getRsBlockTable(),getRsBlockTable()根据QRErrorCorrectLevel.H返回QRRSBlock.RS_BLOCK_TABLE[]数组,也就是我们需要修改的数组。
  9. QRRSBlock.RS_BLOCK_TABLE[]也就是[11,36,12][11,36,12,7,37,13]返回给getRSBlocks()中的局部变量rsBlock后,根据rsBlock生成一堆QRRSBlock对象并放到list[]数组放回。这里添加了后面的7,37,13就是的list增加了11QRRSBlock对象。因为rsBlock.length变成6,所以length等于2,从而外层i循环变成2次遍历,内部j循环遍历后面的7,37,13
  10. 这样list[]返回给上层createData()方法的rsBlocks变量后,createDate()内部统计rsBlocks所有QRRSBlock对象的dataCount,得到totalDataCount,然后拿它和buffer.getLengthInBits()比较。如果它是之前的3位值的数组生成的,遇到长url就会抛出异常,现在我们增加了11dataCount13QRRSBlock对象,总长度就不会小于buffer.getLengthInBits()了,从而解决了抛出异常问题。
  11. 这个buffercreateData()方新建的QRBitBuffer对象,它使用createData()方法的dataList填充。
  12. createData()方法的dataList来自于上层的QRCodeModel对象。在QRCodeModel.makeImpl()方法被调用时,会把QRCodeModelthis.dataList属性赋值给createData()
  13. QRCodeModelthis.dataList来自QRCodeModeladdData()方法填充。
  14. QRCodeModeladdData()方法填充是在新建QRCode对象时调用的,新建QRCode时,会调用this.makeCode(this._htOption.text)function makeCode(sText)内部会调用QRCodeModel.addData(sText),然后才调用QRCodeModel.make(),从而QRCodeModel.make()调用QRCodeModel.makeImpl()
  15. this.makeCode(this._htOption.text)中的this._htOption.text来自QRCode构造方法QRCode = function (el, vOption)中的vOption
  16. QRCode的构造方法的vOption参数来自social-share.js文件createWechat()方法新建QRCode对象时传递的new QRCode(qrcode[0], {text: data.url, width: data.wechatQrcodeSize, height: data.wechatQrcodeSize});。而这个data.url来自于实参function createWechat (elem, data)
  17. 最后,createWechat (elem, data)data来自share(elem, options)方法内部初始化的局部变量var data = mixin({}, defaults, options || {}, dataset(elem));,这里的defaults就是一个对象,它内部有一个url属性,赋值为location.href,也就是当前网页的url。下面就是defaults对象:
    var defaults = {
        url: location.href,
        origin: location.origin,
        source: site,
        title: title,
        description: description,
        image: image,
        imageSelector: undefined,
        weiboKey: '',
        wechatQrcodeTitle: '微信扫一扫:分享',
        wechatQrcodeHelper: '<p>微信里点“发现”,扫一下</p><p>二维码便可将本文分享至朋友圈。</p>',
        wechatQrcodeSize: 100,

        sites: ['weibo', 'qq', 'wechat', 'douban', 'qzone', 'linkedin', 'facebook', 'twitter', 'google'],
        mobileSites: [],
        disabled: [],
        initialized: false
    };

从上面流程我们就可以知道,如果url太长,就会导致微信分享抛出异常,所以我那篇博客起了一个贼长的标题就是一切悲剧的开始。解决的代码是添加了二维码大小,当然,我当时也可以把标题改短一点临时应付。

最后再谈一下这个源码,很明显share(elem, options)如果添加了option,就可以替换默认的defaults中的属性。这个share(elem, options)方法被window.socialShare = function (elem, options) 调用,option参数也是socialShare()option,这个socialShare()alReady()方法调用。

(function (window, document, undefined) {
    ...
    alReady(function () {
        socialShare('.social-share, .share-component');
    });
    /**
     * Dom ready.
     *
     * @param {Function} fn
     *
     * @link https://github.com/jed/alReady.js
     */
    function alReady ( fn ) {
        var add = 'addEventListener';
        var pre = document[ add ] ? '' : 'on';

        ~document.readyState.indexOf( 'm' ) ? fn() :
            'load DOMContentLoaded readystatechange'.replace( /\w+/g, function( type, i ) {
                ( i ? document : window )
                    [ pre ? 'attachEvent' : add ]
                (
                    pre + type,
                    function(){ if ( fn ) if ( i < 6 || ~document.readyState.indexOf( 'm' ) ) fn(), fn = 0 },
                    !1
                )
            })
    }
})(window, document);

我们的代码是这样:

    <link rel="stylesheet" href="/assets/css/share.min.css">
    <script src="/assets/js/social-share.min.js"></script>
    <script>
    var $config = {
        sites : ['weibo', 'wechat', 'qq', 'qzone', 'twitter', 'google'],
    };
    socialShare('.social-share', $config);
    </script>

很清楚,我们可以自定义所有defaults中的属性。

三、解决方法

因为我直接修改的压缩后的js文件,所以全局搜索social-share.min.js文件,把[11,36,12]替换成了[11,36,12,7,37,13],错误就解决了。

img9

然后把微信分享再添加上,没错误了:

img10

img11

这个问题就彻底分析解决了。


知识共享许可协议    鄂ICP备 15002452号-5    鄂公网安备 42088102000048号