吐槽
近日在写微信小程序的一个表单时遇到一个问题,就是小程序原生的一些接口:wx.uploadFile、wx.request(multipart/form-data)无法直接一次性上传多张图片(or文件)。
wx.uploadFile就不用我多说了,一次只能上传一个。
而wx.request就比较特殊,并不是说真的不行,而是不能自动封装multipart/form-data
格式的数据,若在使用wx.request时直接将Content-Type
改成multipart/form-data
,后端会报错,会提示缺乏boundary字段的错误。原因就在于小程序的wx.request不会自动封装multipart/form-data格式的数据,需要你手动封装。如果你自己手动封装,表单参数多起来的话代码就会变得十分难看。
那既然wx.uploadFile和wx.request设置multipart/form-data
都不太管用,那是不是就没办法了?虽然我可以让图片先上传,然后保存服务器回传的url,但我并不想这么做,因为每次上传都是耗费流量(💰)的,要是用户不停上传->删除->上传文件,但就是不提交表单,我离破产就不远了= =、
之后想了半天,可算是给我想到一个解决方案:将图片转base64再发送,且发送Content-Type
为application/json
。
想法源自我用某云sdk的时候,它要求我将文件编码成base64再发送,于是我就想微信小程序是不是也能这么做。
有个地方不得不吐槽,wx.uploadFile明明就是以
multipart/form-data
格式发送的,说明微信小程序官方是完全有能力让wx.request也原生支持multipart/form-data
的,但却一直在这留了坑,难道我不上传文件就一定要自己封装multipart/form-data
请求体?实在让人费解。
实验环境
IDE: 微信开发者工具、IDEA
调试基础库Version: 2.9.5
JDK Version: 1.8
SpringBoot Version: 2.2.2.RELEASE
微信小程序将文件转base64
如果是普通的网页开发,我们可能需要借助一些base64编码的工具类,但在微信小程序就不需要这么麻烦了,基础库为我们提供了足够贴心的服务,有自带的转码接口(包含base64等多种编码),就大大地方便了我们操作,这个接口就是FileSystemManager.readFileSync。
下面来整个表单大概是什么样的:
来看看提交表单的js(在此处转base64):
/**
* 表单提交
*/
submitApply: function(e) {
let params = e.detail.value;
// 校验表单参数
let res = this.checkForm(params);
if (res) {
wx.showModal({
title: '提示',
content: res,
})
return;
}
let imageList = this.data.imgList;
// 图片转base64
const credentialsPhotoFront = 'data:image/jpg;base64,' + wx.getFileSystemManager().readFileSync(imageList[0], 'base64');
const credentialsPhotoReverse = 'data:image/jpg;base64,'+ wx.getFileSystemManager().readFileSync(imageList[1], 'base64');
let realSize1 = credentialsPhotoFront.length - (credentialsPhotoFront.length/8)*2;
let realSize2 = credentialsPhotoReverse.length - (credentialsPhotoReverse.length/8)*2;
// 限制文件大小不超过10Mb
if (realSize1 > app.uploadFileMaxSize || realSize2 > app.uploadFileMaxSize){
wx.showModal({
title: '错误',
content: '单张图片不得超过10Mb',
});
return;
}
params['credentialsPhotoFront'] = credentialsPhotoFront;
params['credentialsPhotoReverse'] = credentialsPhotoReverse;
console.log(params);
let url = app.serverUrl + 'user/applyForCertification';
let token = wx.getStorageSync('token');
let that = this;
wx.showLoading({
title: '提交申请ing'
});
request.requestPostApi(url, token, params, 'application/json', that, that.submitSucc, app.failFun, app.completeFun);
},
- 当然校验文件大小的代码,也可以写到checkForm方法中,我这里是故意贴出来告诉你如何限制文件大小的。
提交表单时,我们会看到控制台有如下输出:
看到这个,基本确认我们的图片成功转码成base64了,剩下的就是后端接收到这些数据,将base64转回图片再上传了,下面我会以SpringBoot做后端框架为例子展示一下我是如何处理的。
java将base64字符转MultipartFile
某个controller部分代码如下:
@Value("${spring.servlet.multipart.max-file-size}")
private DataSize dataSize;
@Override
@PostMapping("applyForCertification")
@CheckLogin
public BaseResponse<UserInfoDTO> applyForCertification(@Valid @RequestBody UserInfoParam userInfoParam) throws Exception {
userInfoParam.setUid(getUid());
long size1 = userInfoParam.getCredentialsPhotoFront().length();
long size2 = userInfoParam.getCredentialsPhotoReverse().length();
long realSize1 = size1 - (size1 / 8) * 2;
long realSize2 = size2 - (size2 / 8) * 2;
long maxFileSize = dataSize.toBytes();
if (realSize1 > maxFileSize || realSize2 > maxFileSize) {
throw new BadRequestException("文件大小不可超过10Mb");
}
return userInfoService.applyForCertification(userInfoParam);
}
- 注意限制文件大小,因为这里使用的是文件的base64编码,而不是直接传文件二进制流,所以
spring.servlet.multipart.max-file-size
参数对这里是没有拦截作用的。 - 其实判断文件大小的代码还不是最严谨的,应该把base64的header和"="号去掉再判断,不过这样做代码是在太难看了,我宁愿把限制的文件大小+1MB😂
- 另外,在网关层我们应该限制请求体的大小,因为上面的代码是拿到文件的base64码之后才能判断。
UserInfoParam
包含两个图片的base64字符串:
// 其余参数略
@ApiModelProperty(value = "正面照base64编码")
@NotBlank(message = "正面照不能为空")
private String credentialsPhotoFront;
@ApiModelProperty(value = "反面照base64编码")
@NotBlank(message = "反面照不能为空")
private String credentialsPhotoReverse;
这两个base64编码后的字符串在业务层处理,图片处理核心代码如下:
MultipartFile credentialsPhotoFront = BASE64DecodedMultipartFile.base64ToMultipart(userInfoParam.getCredentialsPhotoFront());
MultipartFile credentialsPhotoReverse = BASE64DecodedMultipartFile.base64ToMultipart(userInfoParam.getCredentialsPhotoReverse());
UserInfo userInfo = userInfoParam.convertTo();
UploadResult upload1 = fileHandlers.upload(credentialsPhotoFront);
UploadResult upload2 = fileHandlers.upload(credentialsPhotoReverse);
最后再给出BASE64DecodedMultipartFile代码,其实就是将base64编码的字符串重新转回字节流再封装成MultipartFile(代码我也是抄别人的):
import org.springframework.web.multipart.MultipartFile;
import sun.misc.BASE64Decoder;
import java.io.*;
/**
* base64转MultipartFile
*
* @author wenjie
*/
public class BASE64DecodedMultipartFile implements MultipartFile {
private final byte[] imgContent;
private final String header;
public BASE64DecodedMultipartFile(byte[] imgContent, String header) {
this.imgContent = imgContent;
this.header = header.split(";")[0];
}
@Override
public String getName() {
return System.currentTimeMillis() + Math.random() + "." + header.split("/")[1];
}
@Override
public String getOriginalFilename() {
return System.currentTimeMillis() + (int) (Math.random() * 10000) + "." + header.split("/")[1];
}
@Override
public String getContentType() {
return header.split(":")[1];
}
@Override
public boolean isEmpty() {
return imgContent == null || imgContent.length == 0;
}
@Override
public long getSize() {
return imgContent.length;
}
@Override
public byte[] getBytes() throws IOException {
return imgContent;
}
@Override
public InputStream getInputStream() throws IOException {
return new ByteArrayInputStream(imgContent);
}
@Override
public void transferTo(File dest) throws IOException, IllegalStateException {
new FileOutputStream(dest).write(imgContent);
}
/**
* base64转MultipartFile文件
*/
public static MultipartFile base64ToMultipart(String base64) {
try {
String[] baseStrs = base64.split(",");
BASE64Decoder decoder = new BASE64Decoder();
byte[] b = new byte[0];
b = decoder.decodeBuffer(baseStrs[1]);
for (int i = 0; i < b.length; ++i) {
if (b[i] < 0) {
b[i] += 256;
}
}
return new BASE64DecodedMultipartFile(b, baseStrs[0]);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}
再补几张断点调试的图:
确保转字节流没问题了
上传到七牛云也成功了,返回的地址拼装一下再访问,也确实是显示图片
好了,到此为止总算是将一开始的想法都落地实现了,如果还有什么问题欢迎补充。