基于vue+axios+lrz.js微信端图片压缩上传方法


Posted in Javascript onJune 25, 2019

业务场景

微信端项目是基于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 则提供了前端图片文件的压缩方案,并且可以指定尺寸压缩。实测: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、上传组件,未显示上传进度,体验性稍差。

正如前文所述,出于项目实际情况考虑,只是简单实现图片压缩上传功能,如要支持更多的场景,还得细细雕琢。

Javascript 相关文章推荐
jQuery 入门级学习笔记及源码
Jan 22 Javascript
jQuery使用append在html元素后同时添加多项内容的方法
Mar 26 Javascript
jQuery scrollFix滚动定位插件
Apr 01 Javascript
微信小程序 利用css实现遮罩效果实例详解
Jan 21 Javascript
jQuery表单设置值的方法
Jun 30 jQuery
jquery实现下拉菜单的手风琴效果
Jul 23 jQuery
node.js中使用Export和Import的方法
Sep 18 Javascript
JavaScript设计模式之观察者模式(发布订阅模式)原理与实现方法示例
Jul 27 Javascript
js事件on动态绑定数据,绑定多个事件的方法
Sep 15 Javascript
angularJs中ng-model-options设置数据同步的方法
Sep 30 Javascript
vue中使用极验验证码的方法(附demo)
Dec 04 Javascript
vue实现lodop打印功能的示例
Nov 11 Javascript
新手快速入门微信小程序组件库 iView Weapp
Jun 24 #Javascript
前端Vue项目详解--初始化及导航栏
Jun 24 #Javascript
微信小程序调用天气接口并且渲染在页面过程详解
Jun 24 #Javascript
微信小程序-可移动菜单的实现过程详解
Jun 24 #Javascript
webpack自动打包和热更新的实现方法
Jun 24 #Javascript
Promise扫盲贴
Jun 24 #Javascript
深入学习js函数的隐式参数 arguments 和 this
Jun 24 #Javascript
You might like
PHP单态模式简单用法示例
2016/11/16 PHP
PHP中有关长整数的一些操作教程
2019/09/11 PHP
JavaScript中“+=”的应用
2007/02/02 Javascript
做web开发 先学JavaScript
2014/12/12 Javascript
JavaScript制作简易的微信打飞机
2015/03/31 Javascript
node.js版本管理工具n无效的原理和解决方法
2016/11/24 Javascript
JS针对Array的各种操作汇总
2016/11/29 Javascript
Javascript之面向对象--接口
2016/12/02 Javascript
详谈jQuery中使用attr(), prop(), val()获取value的异同
2017/04/25 jQuery
Javascript实现的StopWatch功能示例
2017/06/13 Javascript
浅谈VUE-CLI脚手架热更新太慢的原因和解决方法
2018/09/28 Javascript
WEB前端性能优化的7大手段详解
2020/02/04 Javascript
[02:41]辉夜杯现场一家三口 “我爸玩风行 我玩血魔”
2015/12/27 DOTA
[56:48]FNATIC vs EG 2019国际邀请赛小组赛 BO2 第二场 8.15
2019/08/16 DOTA
Django学习笔记之Class-Based-View
2017/02/15 Python
python ddt实现数据驱动
2018/03/14 Python
python 读文件,然后转化为矩阵的实例
2018/04/23 Python
python3爬取数据至mysql的方法
2018/06/26 Python
用Python shell简化开发
2018/08/08 Python
python斐波那契数列的计算方法
2018/09/27 Python
Python实现的逻辑回归算法示例【附测试csv文件下载】
2018/12/28 Python
python数组循环处理方法
2019/08/26 Python
Python中list循环遍历删除数据的正确方法
2019/09/02 Python
Python递归实现打印多重列表代码
2020/02/27 Python
python GUI库图形界面开发之PyQt5不规则窗口实现与显示GIF动画的详细方法与实例
2020/03/09 Python
Python 程序报错崩溃后如何倒回到崩溃的位置(推荐)
2020/06/23 Python
Python OpenCV中的numpy与图像类型转换操作
2020/12/11 Python
用CSS3实现瀑布流布局的示例代码
2017/11/10 HTML / CSS
Infababy英国:婴儿推车、Travel System婴儿车和婴儿汽车座椅销售
2018/05/23 全球购物
如何安装ruby on rails
2014/02/09 面试题
怎样写好自荐信和推荐信
2013/12/26 职场文书
考试没考好检讨书
2014/01/31 职场文书
2014年班组建设工作总结
2014/12/01 职场文书
甜美蛋糕店的创业计划书模板,拿来即用!
2019/08/21 职场文书
Oracle 死锁的检测查询及处理
2021/09/25 Oracle
Springboot中如何自动转JSON输出
2022/06/16 Java/Android