基于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 相关文章推荐
var与Javascript变量隐式声明
Sep 17 Javascript
Jquery跨域获得Json时invalid label错误的解决办法
Jan 11 Javascript
js实现的仿新浪微博完美的时间组件升级版
Dec 20 Javascript
jQuery之选项卡的简单实现
Feb 28 Javascript
JS实现仿google、百度搜索框输入信息智能提示的实现方法
Apr 20 Javascript
JavaScript实现跨浏览器的添加及删除事件绑定函数实例
Aug 04 Javascript
jQuery实现页面顶部显示的进度条效果完整实例
Dec 09 Javascript
微信小程序分享功能之按钮button 边框隐藏和点击隐藏
Jun 14 Javascript
vue项目中常见问题及解决方案(推荐)
Oct 21 Javascript
JS实现水平移动与垂直移动动画
Dec 19 Javascript
vue中destroyed方法的使用说明
Jul 21 Javascript
maptalks+three.js+vue webpack实现二维地图上贴三维模型操作
Aug 10 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删除非空目录的函数代码小结
2013/02/28 PHP
php+js实现图片的上传、裁剪、预览、提交示例
2013/08/27 PHP
PHP自定义函数获取URL中一级域名的方法
2016/08/23 PHP
使用PHP+Redis实现延迟任务,实现自动取消订单功能
2019/11/21 PHP
JavaScript 学习初步 入门教程
2010/03/25 Javascript
30个最佳jQuery Lightbox效果插件分享
2011/04/11 Javascript
jquery实现的带缩略图的焦点图片切换(自动播放/响应鼠标动作)
2013/01/23 Javascript
js arguments,jcallee caller用法总结
2013/11/30 Javascript
js实现星星打分效果的方法
2020/07/05 Javascript
JS中Location使用详解
2015/05/12 Javascript
JavaScript中字符串(string)转json的2种方法
2015/06/25 Javascript
jQuery验证表单格式的使用方法
2017/01/10 Javascript
详解基于Bootstrap+angular的一个豆瓣电影app
2017/06/26 Javascript
利用js编写网页进度条效果
2017/10/08 Javascript
vue 父组件调用子组件方法及事件
2018/03/29 Javascript
vue项目每30秒刷新1次接口的实现方法
2018/12/04 Javascript
微信小程序仿今日头条导航栏滚动解析
2019/08/20 Javascript
十分钟教你上手ES2020新特性
2020/02/12 Javascript
Node.js API详解之 net模块实例分析
2020/05/18 Javascript
python类:class创建、数据方法属性及访问控制详解
2016/07/25 Python
轻松实现TensorFlow微信跳一跳的AI
2018/01/05 Python
Python常见字典内建函数用法示例
2018/05/14 Python
python使用wxpy实现微信消息防撤回脚本
2019/04/29 Python
Python如何telnet到网络设备
2021/02/18 Python
Mytheresa中国官网:德国时尚奢侈品商城
2017/08/04 全球购物
妇产科护士自我鉴定
2013/10/15 职场文书
求职自荐书范文
2013/12/04 职场文书
安全生产中长期规划实施方案
2014/02/21 职场文书
《最后的姿势》教学反思
2014/02/27 职场文书
经济信息系毕业生自荐信
2014/06/02 职场文书
公共机构节能宣传周活动总结
2014/07/09 职场文书
2015应届毕业生自荐信范文
2015/03/05 职场文书
2015年大学生实习评语
2015/03/25 职场文书
2016年国庆节宣传标语
2015/11/25 职场文书
浅谈Java实现分布式事务的三种方案
2021/06/11 Java/Android
mongodb清除连接和日志的正确方法分享
2021/09/15 MongoDB