业务场景
微信端项目是基于 Vux + Axios 构建的, 关于图片上传的业务场景有以下几点需求:
1, 单张图片上传(如个人头像, 实名认证等业务)
2, 多张图片上传(如某类工单记录)
3, 上传图片时期望能按指定尺寸压缩处理
4, 上传图片可以从相册中选择或者直接拍照
遇到的坑
采用微信 JSSDK 上传图片
在之前开发的项目中(mui + jQuery), 有使用过微信 JSSDK 的接口上传图片, 本想应该能快速迁移至此项目. 事实证明编程没有简单的事:
1, 按指定尺寸压缩图片
JSSDK 提供的接口 wx.chooseImage 是不能指定图片压缩尺寸的, 只能在后端的接口通过 localId 获取图片时, 再转换成指定的尺寸.
2, 微信 JSSDK 的接口权限验证
只要是单页面应用项目, 微信 JSSDK 注入权限验证都会有这个坑, 而这个与路由模式 (hash 或 history) 也有关联. 有关此坑, 后续会再次写文总结. 参考解决方案[微信 JSSDK] 解决 SDK 注入权限验证 安卓正常, iOS 出现 config fail
经过权衡考虑网页可能需要在微信以外的浏览器上也能上传文件, 顾后来放弃了采用微信 JSSDK 接口上传图片的方式.
Android 版微信, input onchange 事件不触发
这个坑, 圈内有很多人踩过了. 在 PC 端测试是正常的, 发布之后, 微信端上传时能选择文件, 但之后没有任何效果. 日志跟踪, 后台的 API 都未调用, 由此判断是 input 的 onchange 事件未被触发.
解决方案, 更改 input 的 accept 属性:
<input ref="file" type="file" accept="image/jpeg,image/png" @change="selectImgs" />
将以上代码更改为:
<input ref="file" type="file" accept="image/*" @change="selectImgs" />
如果不允许从相册中选择, 只能拍照, 增加 capture="camera":
- <input ref="file" type="file" accept="image/*" capture="camera" @change="selectImgs"
- />
- (注: 如果场景支持从相册选择或拍照, 测试发现某些机型拍照后返回到了主页. 哈哈, 也有可能是其他因素引起的问题, 未做深究了)
使用 Lrz.JS 压缩图片
目前手机拍照的图片文件大小一般在 3-4M, 如果在上传时不做压缩处理会相当浪费流量并且占用服务器的存储空间(期望上传原图的另做讨论). 如果能够在前端压缩处理, 那肯定是最理想的方案. 而 lrz.JS https://github.com/think2011/localResizeIMG 则提供了前端图片文件的压缩方案, 并且可以指定尺寸压缩. 实测: 3M 左右的图片文件, 按宽度 450px 尺寸压缩上传后的文件大小在 500kb 左右, 上传时间 2s 以内.
其核心源码, 如下:
- selectImgs () {
- let file = this.$refs.file.files[0]
- lrz(file, { width: 450, fieldName: 'file' }).then((rst) => {
- var xhr = new XMLHttpRequest()
- xhr.open('POST', 'http://xxx.com/upload')
- xhr.onload = () => {
- if (xhr.status === 200 || xhr.status === 304) {
- // 无论后端抛出何种错误, 都会走这里
- try {
- // 如果后端跑异常, 则能解析成功, 否则解析不成功
- let resp = JSON.parse(xhr.responseText)
- console.log('response:', resp)
- } catch (e) {
- this.imageUrl = xhr.responseText
- }
- }
- }
- // 添加参数
- rst.formData.append('folder', 'wxAvatar') // 保存的文件夹
- rst.formData.append('base64', rst.base64)
- // 触发上传
- xhr.send(rst.formData)
- return rst
- })
- }
单个图片上传组件完整代码, 如下(注: icon 图标使用的是 svg-icon 组件):
- <template>
- <div class="imgUploader">
- <section v-if="imageUrl"
- class="file-item">
- <img :src="imageUrl"
- alt="">
- <span class="file-remove"
- @click="remove()">+</span>
- </section>
- <section v-else
- class="file-item">
- <div class="add">
- <svg-icon v-if="!text"
- class="icon"
- icon-class="plus" />
- <span v-if="text"
- class="text">{{text}}</span>
- <input type="file"
- accept="image/*"
- @change="selectImgs"
- ref="file">
- </div>
- </section>
- </div>
- </template>
- <script>
- import lrz from 'lrz'
- export default {
- props: {
- text: String,
- // 压缩尺寸, 默认宽度为 450px
- size: {
- type: Number,
- default: 450
- }
- },
- data () {
- return {
- img: {
- name: '',
- src: ''
- },
- uploadUrl: 'http://ff-ff.xxx.cn/UploaderV2/Base64FileUpload',
- imageUrl: ''
- }
- },
- watch: {
- imageUrl (val, oldVal) {
- this.$emit('input', val)
- },
- value (val) {
- this.imageUrl = val
- }
- },
- mounted () {
- this.imageUrl = this.value
- },
- methods: {
- // 选择图片
- selectImgs () {
- let file = this.$refs.file.files[0]
- lrz(file, { width: this.size, fieldName: 'file' }).then((rst) => {
- var xhr = new XMLHttpRequest()
- xhr.open('POST', this.uploadUrl)
- xhr.onload = () => {
- if (xhr.status === 200 || xhr.status === 304) {
- // 无论后端抛出何种错误, 都会走这里
- try {
- // 如果后端跑异常, 则能解析成功, 否则解析不成功
- let resp = JSON.parse(xhr.responseText)
- console.log('response:', resp)
- } catch (e) {
- this.imageUrl = xhr.responseText
- }
- }
- }
- // 添加参数
- rst.formData.append('folder', this.folder) // 保存的文件夹
- rst.formData.append('base64', rst.base64)
- // 触发上传
- xhr.send(rst.formData)
- return rst
- })
- },
- // 移除图片
- remove () {
- this.imageUrl = ''
- }
- }
- }
- </script>
- <style lang="less" scoped>
- .imgUploader {
- margin-top: 0.5rem;
- .file-item {
- float: left;
- position: relative;
- width: 100px;
- text-align: center;
- left: 2rem;
- img {
- width: 100px;
- height: 100px;
- border: 1px solid #ececec;
- }
- .file-remove {
- position: absolute;
- right: 0px;
- top: 4px;
- width: 14px;
- height: 14px;
- color: white;
- cursor: pointer;
- line-height: 12px;
- border-radius: 100%;
- transform: rotate(45deg);
- background: rgba(0, 0, 0, 0.5);
- }
- &:hover .file-remove {
- display: inline;
- }
- .file-name {
- margin: 0;
- height: 40px;
- Word-break: break-all;
- font-size: 14px;
- overflow: hidden;
- text-overflow: ellipsis;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- }
- }
- .add {
- width: 100px;
- height: 100px;
- float: left;
- text-align: center;
- line-height: 100px;
- font-size: 30px;
- cursor: pointer;
- border: 1px dashed #40c2da;
- color: #40c2da;
- position: relative;
- background: #ffffff;
- .icon {
- font-size: 1.4rem;
- color: #7dd2d9;
- vertical-align: -0.25rem;
- }
- .text {
- font-size: 1.2rem;
- color: #7dd2d9;
- vertical-align: 0.25rem;
- }
- }
- }
- input[type="file"] {
- position: absolute;
- left: 0;
- top: 0;
- width: 100%;
- height: 100%;
- border: 1px solid #000;
- opacity: 0;
- }
- </style>
后端图片存储处理
后端 API 对图片的处理, 是必不可少的环节, 需要将前端提交过来的 base64 字符串转换成图片格式, 并存放至指定的文件夹, 接口返回图片的 Url 路径. 各项目后端对图片的处理逻辑都不一致, 以下方案仅供参考(我们使用 ASP.NET MVC 构建了独立的文件存储站点).
其核心源码, 如下:
- /// <summary>
- /// 图片文件 base64 上传
- /// </summary>
- /// <param name="folder">对应文件夹位置</param>
- /// <param name="base64">图片文件 base64 字符串</param>
- /// <returns></returns>
- public ActionResult Base64FileUpload(string folder, string base64)
- {
- var context = System.Web.HttpContext.Current;
- context.Response.ClearContent();
- // 因为前端调用时, 需要做跨域处理
- context.Response.AddHeader("Access-Control-Allow-Origin", "*");
- context.Response.AddHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
- context.Response.AddHeader("Access-Control-Allow-Headers", "content-type");
- context.Response.AddHeader("Access-Control-Max-Age", "30");
- if (context.Request.HttpMethod.Equals("OPTIONS"))
- {
- return Content("");
- }
- var resultStr = base64.Substring(base64.IndexOf(",") + 1);// 需要去掉头部信息, 这很重要
- byte[] bytes = Convert.FromBase64String(resultStr);
- var fileName = Guid.NewGuid().ToString() + ".png";
- if (folder.IsEmpty()) folder = "folder";
- // 本地上传
- string root = string.Format("/Resource/{0}/", folder);
- string virtualPath = root + fileName;
- string path = Server.MapPath("~" + virtualPath);
- // 创建文件夹
- if (!Directory.Exists(Path.GetDirectoryName(path)))
- {
- Directory.CreateDirectory(Path.GetDirectoryName(path));
- }
- System.IO.MemoryStream ms = new System.IO.MemoryStream(bytes);// 转换成无法调整大小的 MemoryStream 对象
- System.Drawing.Bitmap bitmap = new System.Drawing.Bitmap(ms);
- bitmap.Save(path, System.Drawing.Imaging.ImageFormat.PNG);// 保存到服务器路径
- ms.Close();// 关闭当前流, 并释放所有与之关联的资源
- return Content.NET.Url + virtualPath); // 返回文件路径
- }
结语
由于项目实际情况, 上述的方案中还存在诸多未完善的点:
1, 多张图片上传, 还是采用的与单张图片相同的接口处理, 更为完善的方案是, 前端的多图上传组件只绑定一个关联 Id, 即可通过实现上传和将图片列表查询展示(注: 该功能在微信端未实现).
2, 后端图片上传的接口, 未做严格的安全校验, 更为完善的方案是, 每个上传的场景, 都应该限制文件类型, 限制文件大小, 以及文件数据来源校验(注: 如软件需要按二级等保标准测评, 则后端接口会检测通不过).
3, 上传组件, 未显示上传进度, 体验性稍差.
正如前文所述, 出于项目实际情况考虑, 只是简单实现图片压缩上传功能, 如要支持更多的场景, 还得细细雕琢.
参考
1, 移动端 H5 实现图片上传 https://segmentfault.com/a/1190000010034177
2, 安卓版微信 input onchange 事件不生效
来源: https://www.cnblogs.com/xyb0226/p/11080137.html