写在前面的话
最近又自己在折腾微信小程序了。最新的一个功能中需要实现图片上传。幸运的是,微信小程序扩展能力中有现成的文件上传组件uploader可以使用,而不幸的是,这个组件坑实在太多了,而我又不是单纯的文件上传,还需要同步上传表单数据,因此各种坑,要么就是数据传不过去,要么就是后台取不到数据,折腾了我一天,各种尝试,终于搞定了。前后端完整用法记录一下,希望大家都能快速上手~
uploader介绍
uploader是微信小程序WeUI组件库中的一个图片上传的组件。大家可以在小程序开发文档中——扩展能力中找到相关用法。
这是一个集合了图片选择、上传、预览、删除的完整组件,属性定义也比较全面,可以自定义上传个数,有上传loading提醒和失败提醒,点击预览功能等,基本可以涵盖图片文件上传的所有功能要求。
用法也很简单,在json文件中加入引用后,在wxml文件中直接引入该组件就行,不需要跟自定义的那种文件上传一样,定义一堆标签和样式,方便多了。
官方文档有简单的使用案例:
1.在json中引入uploader组件
2.在wxml中调用该组件,设置属性方法等
<mp-uploader bindfail="uploadError" bindsuccess="uploadSuccess" select="{{selectFile}}" upload="{{uplaodFile}}" files="{{files}}" max-count="5" title="图片上传" tips="图片上传提示"></mp-uploader>
3.定义js中的上传方法
Page({
data: {
files: [{
url: 'http://mmbiz.qpic.cn/mmbiz_png/VUIF3v9blLsicfV8ysC76e9fZzWgy8YJ2bQO58p43Lib8ncGXmuyibLY7O3hia8sWv25KCibQb7MbJW3Q7xibNzfRN7A/0',
}, {
loading: true
}, {
error: true
}]
},
onLoad() {
this.setData({
selectFile: this.selectFile.bind(this),
uplaodFile: this.uplaodFile.bind(this)
})
},
selectFile(files) {
console.log('files', files)
// 返回false可以阻止某次文件上传
},
uplaodFile(files) {
console.log('upload files', files)
// 文件上传的函数,返回一个promise
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('some error')
}, 1000)
})
},
uploadError(e) {
console.log('upload error', e.detail)
},
uploadSuccess(e) {
console.log('upload success', e.detail)
}
});
这部分代码里面其实只需要补充uplaodFile上传方法调用后台上传图片的接口,上传功能就算完整了,这算是一个可用的完整Demo。但是实际使用起来,还是需要完善一下滴。废话不多说,直接上代码~
用法与代码
小程序前端
1.如前文所说,在json中引入组件,在页面调用
<mp-uploader bindfail="uploadError" bindsuccess="uploadSuccess" select="{{selectFile}}" upload="{{uplaodFile}}" files="{{files}}" max-count="1" title=""></mp-uploader>
因为我只需要上传一张图片,因此设置max-count
等于1。
2.在uplaodFile中,需要调用resolve({urls})方法设置上传成功状态,否则图片会如下图所示一直显示在加载中,体验很不友好,因此先“假装”已经上传成功,等之后提交表单时再真正上传到后台。
修改uplaodFile方法,调用resolve({urls})方法设置上传成功状态,保存临时文件目录tempFilePaths(后面会用到)
uplaodFile(files) {
console.log('upload files', files);
var that = this;
// 文件上传的函数,返回一个promise
return new Promise((resolve, reject) => {
const tempFilePaths = files.tempFilePaths;
that.setData(
{
filesUrl: tempFilePaths
}
)
var object = {};
object['urls'] = tempFilePaths;
resolve(object);
})
},
此时图片会正常显示:
3.在表单提交方法中调用文件上传接口。
在微信小程序中,有一个wx.uploadFile的API方法,用来将本地资源上传到服务器,同时这个方法还能上传HTTP 请求中其他额外的 form data,刚好满足我的需求。
在通用的app.js文件中定义了uploadFile 方法,参数url为后台接口路径,filePath是本地图片路径,param则是需要上传的表单数据。
///上传单个文件
const uploadFile = (url, filePath,param) => {
return new Promise((resolve, reject) => {
wx.uploadFile({
url: url, //仅为示例,非真实的接口地址
filePath:filePath,
name: 'file',
formData: param,
success (res){ //上传成功
console.log(res)
//成功调用接口
resolve(JSON.parse(res.data));
},
fail(err){
console.log(err)
wx.showToast({ title: '请求失败,请刷新后重试', icon: 'none' });
reject(err)
}
})
})
}
在页面调用uploadFile方法
submitForm: function () {
this.selectComponent('#form').validate((valid, errors) => {
if (!valid) { //数据校验
const firstError = Object.keys(errors)
if (firstError.length) {
this.setData({
error: errors[firstError[0]].message
})
}
} else { //校验通过,保存
var that = this;
var url='/api/TestAPI/Add'; //后台接口地址
var filePath=that.data.filesUrl[0];
var formData={ //表单数据
'_Name': that.data.formData._Name,
'_Description': that.data.formData._Description,
'_Type': that.data.formData._Type,
'_IsVisible': that.data.formData._IsAllVisible.toString(), //Boolean类型
'_Tips': JSON.stringify(that.data.formData._tips) //Array类型
};
api.uploadFile(url,filePath,formData).then((res) => { //上图图片并保存表单
if (res.Code == "Success") {
wx.showToast({
title: '添加成功'
});
wx.navigateBack({ //返回上一页
delta: 1,
})
}
})
.catch((err) => {
wx.showToast({
title: '保存失败'
})
})
}
})
},
大家看上面的代码,可以发现我将formData数据重新赋值了一次,并且做了一次类型转化。为什么要这么麻烦呢?直接一个formData扔过去不行吗?——答案是不行,采坑记录1,后面再细说。反正这么写之后图片和数据都可以传过去了,后台再接收就可以了。
后台接口 WebAPI
后台接口我采用的是WebAPI框架,可以自动生成基于RESTful标准的接口帮助文档,很方便使用。
WebApi的接口参数有两种形式,一种是基于url的[FromUri]
,另一种则是基于表单数据的[FromBody]
。在我之前不需要上传文件,直接通过post请求获取前端表单数据时,只要使用[FromBody]
参数就可以直接将前端的Json对象转化成实体类,很简单易用。
然而使用wx.uploadFile上传数据之后,不再能接收到[FromBody]
参数。因此接口方法也需要做些调整。代码如下:
[HttpPost]
[Route("Add")]
public ApiResultModel Add()
{
ApiResultModel result = new ApiResultModel() { Code = APIReturnCode.Error.ToString(), Message = "添加失败" };
var hole = new TreeHole();
HttpContextBase context = (HttpContextBase)Request.Properties["MS_HttpContext"];//获取传统context
HttpRequestBase request = context.Request;//定义传统request对象
hole.Name = request.Form["_Name"];
hole.Description = request.Form["_Description"];
hole.Type = Guid.Parse(request.Form["_Type"]);
hole.IsAllVisible = Boolean.Parse(request.Form["_IsAllVisible"]); //获得boolean类型数据
var obk = request.Form["_tips"];
var tips = JsonConvert.DeserializeObject<JArray>(obk);//获得Array类型数据
XTransaction tran = SessionManager.Instance.BeginTransaction();
try
{
string url;
bool isUploaded = uploadImage(out url); //保存图片
if (!isUploaded)
{
result.Message = "上传图片失败";
return result;
}
hole.Id = Guid.NewGuid();
hole.CreateTime = DateTime.Now;
hole.IsDelete = false;
hole.HeadImage = url;
//保存代码(略)
result.Message = "添加成功!";
result.Code = APIReturnCode.Success.ToString();
result.Token = hole.Id.ToString();
}
catch (Exception ex)
{
if (tran != null) tran.RollbackTransaction();
result.Message = ex.Message;
}
finally
{
tran.Dispose();
}
return result;
}
其中图片上传方法如下:
/// <summary>
/// 上传图片
/// </summary>
/// <param name="imageUrl"></param>
/// <returns></returns>
public bool uploadImage(out string picturePath)
{
picturePath = "";
try
{
const string fileTypes = "gif,jpg,jpeg,png,bmp";//允许上传的图片文件格式
var content = Request.Content;//获取或设置 HTTP 消息的内容(当需要获取HTTP信息是会使用到)
const string tempUploadFiles = "/UploadFile/"; //保存路径
var newFilePath = DateTime.Now.ToString("yyyy-MM-dd") + "/";
var memoryStreamProvider = new MultipartMemoryStreamProvider();//获取文件流信息
Task.Run(async () => await Request.Content.ReadAsMultipartAsync(memoryStreamProvider)).Wait(); //读取数据
foreach (var item in memoryStreamProvider.Contents)
{
if (item.Headers.ContentDisposition.FileName == null) continue; //判断数据类型
var filename = item.Headers.ContentDisposition.FileName.Replace("\"", "");//这里获取含有双引号'" ',需去掉
var file = new FileInfo(filename);
//upload(判断是否是运行上传的图片格式)
if (Array.IndexOf(fileTypes.Split(','), file.Extension.Substring(1).ToLower()) == -1)
{
return false;
}
//获取后缀
var extension = Path.GetExtension(filename);
var newFileName = Guid.NewGuid().ToString() + extension;//重命名
if (!Directory.Exists(HostingEnvironment.MapPath("/") + tempUploadFiles + newFilePath))
{
Directory.CreateDirectory(HostingEnvironment.MapPath("/") + tempUploadFiles + newFilePath);
}
var filePath = Path.Combine(HostingEnvironment.MapPath("/") + tempUploadFiles + newFilePath, newFileName);
picturePath = Path.Combine(tempUploadFiles + newFilePath, newFileName);//图片相对路径
var result = item.ReadAsStreamAsync().Result;
using (var br = new BinaryReader(result))
{
var data = br.ReadBytes((int)result.Length);
File.WriteAllBytes(filePath, data);//保存图片
}
}
//保存成功
return true;
}
catch (Exception ex)
{
return false;
}
}
采坑记录
采坑记录一:使用wx.uploadFile中的formData传递表单数据时,Boolean类型、数组等无法传递到后台。
wx.uploadFile是客户端发起一个 HTTPS POST 请求,其中 content-type 为 multipart/form-data。按照之前的开发经验,直接将表单对象序列化,整个扔过去就行了。如下图所示:
然而小程序中没有serialize方法,直接传formData,在后台接收时会出现如下问题:
因此我只能采用最简单最原始的方法,将表单对象中无法传递的类型转成字符串的形式,后台接收到之后再转化一下。其中Boolean类型直接toString
就好了,数组类型则需要使用JSON.stringify()
转换:
'_IsVisible': that.data.formData._IsAllVisible.toString(),
'_Tips': JSON.stringify(that.data.formData._tips)
采坑记录二:WebAPI接口 MultipartFormDataStreamProvider.FormData内容为空
因为wx.uploadFile的 content-type 为 multipart/form-data类型为空,所以接口中需要通过MultipartMemoryStreamProvider来获取数据。我在网上寻找参考案例时,发现很多都是如下写法:
string root = HttpContext.Current.Server.MapPath("~/upload/TreeHole");
if (!System.IO.File.Exists(root))
{
Directory.CreateDirectory(root);
}
var provider = new MultipartFormDataStreamProvider(root);
foreach (var key in provider.FormData.AllKeys)
{
foreach (var val in provider.FormData.GetValues(key))
{
str += string.Format("{0}: {1}", key, val);
}
}
通过遍历MultipartFormDataStreamProvider中的FormData键值来获取数据。然后我在实际操作过程中,发现这样根本就去不到数据,FormData为Null! 我不知道是传值还是哪里出了问题,最后还是通过Request来获取值。
HttpContextBase context = (HttpContextBase)Request.Properties["MS_HttpContext"];//获取传统context
HttpRequestBase request = context.Request;//定义传统request对象
hole.Name = request.Form["_Name"];
hole.Description = request.Form["_Description"];
hole.Type = Guid.Parse(request.Form["_Type"]);
hole.IsAllVisible = Boolean.Parse(request.Form["_IsAllVisible"]); //获得boolean类型数据
var obk = request.Form["_tips"];
var tips = JsonConvert.DeserializeObject<JArray>(obk);