我的博客分享功能使用的是share.js这个项目,它的微信分享功能使用的是QRCodejs这个项目。然后就在昨天发表了《Centos7从初次登陆服务器到安装nginx配置https全解》后,噩梦开始了。。
一、发现错误应急调试
昨天兴高采烈的写完《Centos7从初次登陆服务器到安装nginx配置https全解》后,和平时一样,使用了bundle exec jekyll build
把markdown
写的博客编译成了静态网页,一切正常。然后我用rsync
把生成好的网页同步到了服务器上。和平时一样,上传完博客后我就用Chrome
浏览器打开网站欣赏一番,然后f12
打开调试工具,按下f5
刷新一下,愉悦的感受一下博客的网速。然而就在网页刷新那一刻,Console
的报错打破了我平静的生活:
虽然那一瞬间心中一阵诧异,但马上回过神来。“这是在羞辱老夫”,我不禁喃喃自语。毕竟我纵横互联网五百年,什么风浪没见过。一眼瞟去错误信息,发现是social-share.min.js
抛出了异常。“应该是分享插件出了问题”,心中一瞬间定位出了方向,然后我把页面拖到最下面看看分享按钮:
“靠,什么时候出的问题”,我不经骂道。于是我赶紧点开了前几次写的博客:
“嘶~没问题啊”,我又回到出问题的博客,刷新几下依然有问题:
此时的我,一阵疑惑。但作为有多年开发的我,经验毕竟是在这,就像多年的战士一般,单刀赴会,直奔主题。
我看了一下抛出的异常栈,先看看最近的地方at Function.e.createData (social-share.min.js:1)
,于是我直接拿createData
去未压缩的源码搜索,发现都是在qrcode.js
里,方法是QRCodeModel.createData
,因为对象名压缩后QRCodeModel
会变成很短的字符,看看压缩文件前后代码可以分析出来位置。其实也就有两处调用而已,我马上就找到抛出异常的位置了:
“既然是QRCodeModel
的方法,那就再看看它是什么吧”,我这么想到,于是我又开始搜索它,虽然有些多,但也马上找到一些蛛丝马迹了:
在QRCode
的makeCode
方法里新建了一个它的对象,QRCode
和makeCode
方法是熟悉的,它就在前面浏览器抛出的异常调用栈里。再看看QRCode
是哪里用的,很快我也找到位置了,在social-share.js
文件里:
看看这里的代码,发现有wechat
字样,div
标签里也有wechat-qrcode
,大致明白了是微信分享的二维码出了问题,异常信息就是“代码长度溢出”,说明在生成分享二维码的时候,输入给QRCode
的内容太多导致抛出异常。先不管那么多,让线上正常再说,把微信分享先去掉:
1
2
3
4
5
6
7
8
9
10
11
12
<!-- 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 -->
已经没有报错了:
微信分享被去掉了:
嗯,一切都正常,赶紧编译上传到服务器里,然后刷新一下CDN缓存。昨天就惊心动魄就此结束了。
二、分析问题
昨天是治标没治本,毕竟微信分享功能没了,不爽。本着较真的性格,今天就来彻底解决问题。其实也没有什么其他方法,这个毕竟是开源项目,既然我遇到了,其他人也应该会遇到,所以用Google总能搜索到什么。于是我拿异常信息去搜索,果然是找了解决方法。
看看修复的代码,其实也就是修改了下QRRSBlock.RS_BLOCK_TABLE
数组,把[11,36,12]
换成了[11,36,12,7,37,13]
,也就是在原有的数组上增加了些内容,这个数组的意思是:[count, totalCount, dataCount]
或[count, totalCount, dataCount, count, totalCount, dataCount]
,增加后三位就是增加了大小。再看看抛出异常时的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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()
是什么:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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()
是什么:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
的值简单的返回我们修改的数组而已。再看看这个值从哪来:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
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]);
}
从上面代码从内部往上剖析找到数据,然后再从外往内解释就很清楚了。
- 首先如果我们打开了微信分享,
socialShare
会调用createWechat()
生成二维码,createWechat()
内部会新建QRCode
对象。 - 新建的
QRCode
对象会初始化属性this._htOption
,this._htOption
包含correctLevel : QRErrorCorrectLevel.H
属性,并且调用this.makeCode()
方法。 makeCode()
方法内部会新建QRCodeModel
对象,并把上层的correctLevel
属性传递进去this._oQRCode = new QRCodeModel(_getTypeNumber(sText, this._htOption.correctLevel), this._htOption.correctLevel);
,this._htOption.correctLevel
也就是QRErrorCorrectLevel.H
。- 在
function QRCodeModel(typeNumber, errorCorrectLevel)
的初始化方法中,会把上层的this._htOption.correctLevel
赋值给方法参数errorCorrectLevel
,然后在内部赋值给自己的属性this.errorCorrectLevel = errorCorrectLevel;
,这时,this.errorCorrectLevel
也就是QRErrorCorrectLevel.H
了。 makeCode()
方法新建QRCodeModel
对象后,会再调用QRCodeModel
对象的make()
方法,make()
方法进而会调用makeImpl()
方法,而makeImpl()
方法会调用QRCodeModel.createData()
方法,并把this.errorCorrectLevel
属性传递给createData()
,此时QRErrorCorrectLevel.H
就传递进入了createData()
方法中,createData()
的方法定义:QRCodeModel.createData = function (typeNumber, errorCorrectLevel, dataList) {...}
。createData()
方法中,会调用QRRSBlock.getRSBlocks()
方法,并把errorCorrectLevel
参数传递进去。getRSBlocks()
方法又会调用QRRSBlock.getRsBlockTable()
,并把errorCorrectLevel
参数传递进去。- 此时,
QRErrorCorrectLevel.H
就传递给了getRsBlockTable()
,getRsBlockTable()
根据QRErrorCorrectLevel.H
返回QRRSBlock.RS_BLOCK_TABLE[]
数组,也就是我们需要修改的数组。 QRRSBlock.RS_BLOCK_TABLE[]
也就是[11,36,12]
或[11,36,12,7,37,13]
返回给getRSBlocks()
中的局部变量rsBlock
后,根据rsBlock
生成一堆QRRSBlock
对象并放到list[]
数组放回。这里添加了后面的7,37,13
就是的list
增加了11
个QRRSBlock
对象。因为rsBlock.length
变成6
,所以length
等于2
,从而外层i
循环变成2
次遍历,内部j
循环遍历后面的7,37,13
。- 这样
list[]
返回给上层createData()
方法的rsBlocks
变量后,createDate()
内部统计rsBlocks
所有QRRSBlock
对象的dataCount
,得到totalDataCount
,然后拿它和buffer.getLengthInBits()
比较。如果它是之前的3
位值的数组生成的,遇到长url
就会抛出异常,现在我们增加了11
个dataCount
为13
的QRRSBlock
对象,总长度就不会小于buffer.getLengthInBits()
了,从而解决了抛出异常问题。 - 这个
buffer
是createData()
方新建的QRBitBuffer
对象,它使用createData()
方法的dataList
填充。 createData()
方法的dataList
来自于上层的QRCodeModel
对象。在QRCodeModel.makeImpl()
方法被调用时,会把QRCodeModel
的this.dataList
属性赋值给createData()
。QRCodeModel
的this.dataList
来自QRCodeModel
的addData()
方法填充。QRCodeModel
的addData()
方法填充是在新建QRCode
对象时调用的,新建QRCode
时,会调用this.makeCode(this._htOption.text)
,function makeCode(sText)
内部会调用QRCodeModel.addData(sText)
,然后才调用QRCodeModel.make()
,从而QRCodeModel.make()
调用QRCodeModel.makeImpl()
。this.makeCode(this._htOption.text)
中的this._htOption.text
来自QRCode
构造方法QRCode = function (el, vOption)
中的vOption
。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)
。- 最后,
createWechat (elem, data)
的data
来自share(elem, options)
方法内部初始化的局部变量var data = mixin({}, defaults, options || {}, dataset(elem));
,这里的defaults
就是一个对象,它内部有一个url
属性,赋值为location.href
,也就是当前网页的url
。下面就是defaults
对象:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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()
方法调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
(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);
我们的代码是这样:
1
2
3
4
5
6
7
8
<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]
,错误就解决了。
然后把微信分享再添加上,没错误了:
这个问题就彻底分析解决了。