前端直接生成及下载二维码和使用七牛SDK上传图片

最近工作比较紧张,除了日常的小需求之外,还做了一个给商户用的后台。技术栈除了vue之外,还用到了materialize这个UI库地址,但是这个库有一个缺点,那就是强依赖与jquery。

因为是后台,依赖juqery倒也没什么,因为是在pc端使用对文件大小的要求不是特别高,而且基于juqery的插件特别多,有些地方如果自己去处理可能会存在一些不兼容的问题,用插件会避免很多怪异的问题。

由于这个业务交互不是特别复杂,其中有两点可以分享的:一个就是前端处理二维码及下载,只需要从后端获取到生成二维码的url,就可以生成二维码以及把他下载下来,不需要后台的处理;另一个是应用七牛的SDK去上传图片,其上传也是从前端来完成,不需要后端处理。


前端直接生成二维码及下载

生成二维码这个没什么,就是一个生成二维码的库地址这个库是基于jquery的,github上还有不依赖于jquery的库。这样二维码就生成了。

下载二维码,原理是这样的:把生成的img标签利用canvas的toDataURL()方法转变为base64格式的,然后把base64的串放到a标签的href属性中。利用a标签的download属性,就可以把它下载下来。如果不仅仅需要二维码而是需要在二维码中加入一些文字或者背景图的话,就可以利用canvas把二维码和需要的文字背景图都绘制到canvas中,再获取这个canvas的toDataURL(),再依据上面的方法去下载下来。

不过这种方法也有不足的地方,比如说批量下载。这种方法如果需要批量下载就必须依靠js,然后去遍历a标签去主动触发click事件。

下面贴一下代码就好咯~

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
//生成二维码
//可能代码和业务会有一定关联,我把大部分关联的都删掉了

$.ajax({
//请求要生成url接口
url : 'http://172.100.101.106:9099/merchant_qrcode/dccode',
type : 'POST',
dataType: 'json',
data : post_data,
success:function(data){
$("#qrcode_box").html("");
if(data['respcd'] == '0000') {
var qrcodes = data.data.qrcode_list;
for(var i=0;i<qrcodes.length;i++)
{
var box_label = $("<label for='"+qrcodes[i]['num']+"'></label>");
var img_panel = $("<div class='col-lg-4 col-md-4 col-sm-6'></div>");
var checkbox = $("<input class='chk' type='checkbox' id='"+qrcodes[i]['num']+"' />")[0];

var new_panel = $("<div class='bg-panel'></div>");
var new_elem = $("<div class='qrcode'></div>")[0];
var new_desc = $("<div class='text-desc text-center'><span>"+qrcodes[i]['area_name']+" "+qrcodes[i]['num']+"</span></div>")[0];
var label = $("<label for='"+qrcodes[i]['num']+"'></label>");

//拼接dom包含checkbox,显示二维码的div,文字信息的div等
new_panel.append(checkbox);
new_panel.append(label);
new_panel.append(new_elem);
new_panel.append(new_desc);

//生成下载按钮,把一些信息保存到标签的data属性里,比如要拼接到canvas中的文字和url信息等等。
var btn_panel = $("<div class='btn-panel'></div>");
var download_elem = $("<div style='display:inline-flex;'><button class='m-l btn btn-primary download_pngcode' data-num='"+qrcodes[i]['num']+"' data-qrcode='"+qrcodes[i]['qrcode']+"' data-area_name='"+qrcodes[i]['area_name']+"' onclick=download_qrcode_png(this) >下载桌贴</button><button class='m-l download_code btn btn-warning' data-num='"+qrcodes[i]['num']+"' data-qrcode='"+qrcodes[i]['qrcode']+"' data-area_name='"+qrcodes[i]['area_name']+"' onclick=download_qrcode(this) >下载二维码</button></div>");
btn_panel.append(download_elem);

img_panel.append(new_panel);
img_panel.append(btn_panel);
box_label.append(img_panel)
//生成二维码
var qrcode = new QRCode(new_elem,{
width : 131,
height: 131
});
qrcode.makeCode(qrcodes[i].qrcode);
}
_this.btnDisabled
}
}
});

//下载二维码

//仅下载二维码
exports.download_qrcode = function (e){
var new_elem = $("<div></div>")[0];
var qrcode = new QRCode(new_elem,{
width : 262,//设置宽高
height : 262
});
console.log($(e));
qrcode.makeCode($(e).data('qrcode'));
var c=document.createElement('canvas'),ctx=c.getContext('2d');
var qr_ele = qrcode;
c.width=262;
c.height=310;
ctx.rect(0,0,c.width,c.height);
ctx.fillStyle='#fff';
ctx.fill();
var img = new Image;
img.crossOrigin = 'Anonymous'; //解决跨域
img.src = qrcode._el.children[0].toDataURL();
ctx.drawImage(img,0,0,262,262);

ctx.fillStyle='#fe9b20';
ctx.font = "bold 32px 黑体";
ctx.textAlign = "center";

//在二维码下方添加文字
var fill_txt = "";
if($(e).data("area_name") !=""){
fill_txt = $(e).data("area_name")+" ";
}
if($(e).data("num") != ""){
fill_txt += ($(e).data("num") + '号桌');
}
ctx.fillText(fill_txt,131,300);

//读取生成标签中的data属性,生成下载a标签
var $a = $("<a></a>").attr("href", c.toDataURL()).attr("download", $(e).data("area_name")+$(e).data("num")+".png");
$a[0].click();
}

//下载带背景图的二维码
exports.download_qrcode_png = function (e){
var new_elem = $("<div></div>")[0];
var qrcode = new QRCode(new_elem,{
width : 262,//设置宽高
height : 262
});
qrcode.makeCode($(e).data('qrcode'));
var c=document.createElement('canvas'),ctx=c.getContext('2d');
var qr_ele = qrcode;
c.width=460;
c.height=620;
ctx.rect(0,0,c.width,c.height);
ctx.fill();
//读取背景图
var bg_img = $("#background_image")[0];
var img = new Image;
img.crossOrigin = 'Anonymous'; //解决跨域
img.src = qrcode._el.children[0].toDataURL();
//画到canvas中
ctx.drawImage(bg_img,0,0,460,620);
ctx.drawImage(img,98,162,262,262);

ctx.fillStyle='#fe9b20';
ctx.font = "bold 32px 黑体";
ctx.textAlign = "center";

var fill_txt = "";
if($(e).data("area_name") !=""){
fill_txt = $(e).data("area_name")+" ";
}
if($(e).data("num") != ""){
fill_txt += ($(e).data("num") + '号桌');
}
ctx.fillText(fill_txt,230,465);

//document.body.appendChild(c);

var $a = $("<a></a>").attr("href", c.toDataURL()).attr("download", $(e).data("area_name")+$(e).data("num")+".png");
$a[0].click();
}

使用七牛SDK上传图片

七牛的开发文档地址这里。

按照七牛文档和demo就可以很快实现一个上传图片的功能。我来说一下上传的大概过程和我遇到的一个问题。

首先服务端的会先做一些工作,这个我们不太需要关心只要知道我们在上传图片之前,需要先向服务端去发起一次请求去拿到token和key,因为可以看到七牛上传图片时是需要这两个参数的。然后开始上传图片,上传图片后还有一个成功回调事件,在回调事件中可以做一些事情。(告知服务端图片上传成功了,并把这张图片的url告诉服务端,这样才能把图片对应起来)

这里这再插一句,七牛的SDK使用的plupload插件是这样的:他在初始化的时候需要的参数有token,按钮的id,还有一个可选的参数key(这个参数是可以把文件的url替换成你自定义的)。然后会在方法执行时会去寻找你这个id的button,去初始化它,在这个标签下面再去创建一个input标签,在你点击这个id的button的时候他会去隐式的点击下面的input来唤起浏览器选择文件。

好,大概的过程就是这样的。没有什么问题,但是不管文档和demo都只说了只有一个按钮的情况。如果我有一个列表,上面有很多上传按钮这种情况该怎么办?文档上有类似问题:地址。但是他这里说的情况是固定的几个按钮,并且知道他的token和key,还有按钮标签的id

这种是不符合我目前需求的,因为我目前整个列表有几个上传按钮都是不确定的,按钮的id等等也是不固定的,我要等请求后端接口拿到token和key之后,再去初始化按钮,并且每个按钮和列表每一行的商品要一一对应。所以在完成这个初始化多个上传按钮时碰到了问题。

我们分析一下:如果需要一一对应那么我必须使每一行的button都有自己单独的id、token、和key(因为需要自定义上传图片的url)。

最开始我是这么写的:(大概过程可以看注释)

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
//请求商品列表
ajax({
url: 'http://172.100.101.106:9099/goods/list',
method: 'GET',
data: data,
callback: function (res) {
//把初始化队列置空
_this.$set('initList', [])
if (res.respcd === '0000') {
_this.$set('goodsList', res.data.list)
//遍历商品列表的每一项,创建一个新的对象包含初始化时需要的id,然后用unionid关联商品列表和新建的初始化列表
for (let i = 0, len = res.data.list.length; i < len; i++) {
if (res.data.list[i].img === '') {
let a = {
id: 'img-id-' + res.data.list[i].id,
unionid: res.data.list[i].unionid
}
//把对象push进初始化列表
_this.initList.push(a)
}
}
}
//这个是vue监听dom渲染完成的事件(可忽略)
_this.$nextTick(function () {
//初始化列表的每一项进行初始化按钮的操作
for (let i = 0, len = _this.initList.length; i < len; i++) {
_this.initUpload(_this.initList[i].unionid, _this.initList[i].id)
}
})
}
})

//方法接受两个参数
//unionid 这个参数是最后上传完成后给服务端发请求告知 是哪个商品的图片,让服务端把商品和图片对应起来;
//id 就是初始化按钮是需要的id;
initUpload (unionid, id) {
let _this = this
//这个是获取token和key的接口,上面过程说明了一下
$.ajax({
url: 'https://xxxxxxxxx/qiniu_token',
type: 'get',
dataType: 'json',
data: {
'appcode': 'xxxxxx',
'func': 'upload',
'format': 'cors'
},
async: false,
timeout: 3000,
success: function (data) {
if (data['respcd'] === '0000') {
let uptoken = data.data.token
******************
let key = data.data.key
******************
var uploader = Qiniu.uploader({
runtimes: 'html5,flash,html4', //上传模式,依次退化
browse_button: id, //上传选择的点选按钮,**必需**
uptoken: uptoken,
domain: 'https://xxxxxxxxx.com/', //bucket 域名,下载资源时用到,**必需**
get_new_uptoken: true, //设置上传文件的时候是否每次都重新获取新的token
max_file_size: '10mb', //最大文件体积限制
flash_swf_url: './flash/Moxie.swf', //引入flash,相对路径
max_retries: 3, //上传失败最大重试次数
dragdrop: true, //开启可拖曳上传
drop_element: id, //拖曳上传区域元素的ID,拖曳文件或文件夹后可触发上传
chunk_size: '4mb', //分块上传时,每片的体积
auto_start: true, //选择文件后自动上传,若关闭需要自己绑定事件触发上传
init: {
'FilesAdded': function (up, files) {
plupload.each(files, function (file) {
// 文件添加进队列后,处理相关的事情
})
},
'BeforeUpload': function (up, file) {
// 每个文件上传前,处理相关的事情
},
'UploadProgress': function (up, file) {
// $('#img_upload-' + rand_num).addClass('spinner')
// 每个文件上传时,处理相关的事情
},
'FileUploaded': function (up, file, info) {
var domain = up.getOption('domain')
var res = JSON.parse(info)
var sourceLink = domain + res.key //获取上传成功后的文件的Url

$.ajax({
url: 'http://172.100.101.106:9099/goods/setimg',
type: 'POST',
dataType: 'json',
data: {
'unionid': unionid,
'img': sourceLink
},
success: function (data) {
if (data['respcd'] === '0000') {
window.Materialize.toast('添加商品图片成功', 4000)
_this.getUserInfo(_this.currentPage, _this.pageSize)
}
},
error: function (data) {
window.Materialize.toast(data.respmsg, 4000)
}
})
},
'Error': function (up, err, errTip) {
//上传出错时,处理相关的事情
window.Materialize.toast(errTip, 4000)
$('#img_upload').removeClass('spinner')
},
'UploadComplete': function () {
//队列文件处理完毕后,处理相关的事情
},
'Key': function (up, file) {
*************
return key
*************
}
}
})
}

请注意我标**的那几行,我本意是把每一个ajax请求到的key,赋到每一个初始化按钮的方法中去,但是如果我们实际这么去写的话,token是对的,但是key总是最后一个请求的key,原因就是初始化中'Key'这个方法,并不是在方法初始化的时候就去执行了,而是你再点击这个按钮的时候再去获取这个key的值,所以当你点击按钮时这个key的值就是最后一次请求到key的值了。(只有一个key会造成上传了多张图片,但是你只会有一个url的情况发生)

那如何去在一个页面用七牛的SDK去初始化N个上传按钮呢?

我当时试了闭包这种方法,大概是这样:

1
2
3
4
(function (key) {
//初始化
var uploader = Qiniu.uploader({})
})(key)

但是并不可以。

找到了一种变通式的解决方法,就是在你每次点击按钮时,去渲染一个新的DOM,再用这个新的DOM去执行初始化的方法:

1
2
3
4
5
6
var rand_num = parseInt(Math.random()*100000000)
$("#upload_block").html("");
var html ='<div id="img-container-'+rand_num+'" class="dz-default dz-message"><button id="img_upload-'+rand_num+'" class="btn btn-default btn-lg"><i class="glyphicon glyphicon-plus"></i><span>上传图片</span></button></div>';
$("#upload_block").html(html);

//然后把这个DOM进行初始化

这样同步的来进行,一次只有一个token和key,初始化一个按钮,这样就不会存在上面的问题,但是这样就意味着你点击上传按钮之后,又会出来一个蒙层点击蒙层上的按钮才可以选择图片,这样体验未免有些太差了。

解决

最后经过一下午的尝试,最后也是受到了demo的启发:

1
2
3
4
5
6
7
8
9
10
11
12
'FileUploaded': function(up, file, info) {
// 每个文件上传成功后,处理相关的事情
// 其中info是文件上传成功后,服务端返回的json,形式如:
// {
// "hash": "Fh8xVqod2MQ1mocfI4S4KpRL6D98",
// "key": "gogopher.jpg"
// }
// 查看简单反馈
// var domain = up.getOption('domain');
// var res = parseJSON(info);
// var sourceLink = domain + res.key; 获取上传成功后的文件的Url
}

七牛给的上传说明中有这么一段,方法的第一个参数是up,通过注释我们可以看出这个参数可以获取到Qiniu.uploader这个方法的一些配置参数的值,比如这一行var domain = up.getOption('domain');他就获取到了domain这个配置的值(即https://xxxxxxxxx.com/),可以看到'Key'这个方法也会接受up这个参数,所以可不可以在配置中多加一个keyValue的多余配置,来储存key值呢?

经过试验答案是可以的!大概这样:(相同代码就不再重复)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var uploader = Qiniu.uploader({
keyValue: key,

.....


init: {

.....

'Key': function (up, file) {
let key = up.getOption('keyValue')
return key
}
}

其他地方不变,这样经过试验可以很好地解决初始化多个按钮的问题!