基于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 相关文章推荐
JavaScript 语法集锦 脚本之家基础推荐
Nov 15 Javascript
一个分享按钮的插件使用介绍(可扩展,内附开发制作流程)
Sep 19 Javascript
图片翻转效果具体实现代码
Jan 09 Javascript
jquery操作复选框checkbox的方法汇总
Feb 05 Javascript
jQuery+AJAX实现网页无刷新上传
Feb 22 Javascript
jQuery中$this和$(this)的区别介绍(一看就懂)
Jul 06 Javascript
JS类的定义与使用方法深入探索
Nov 26 Javascript
jQuery实现在新增加的元素上添加事件方法案例分析
Feb 09 Javascript
详解javascript 正则表达式之分组与前瞻匹配
May 30 Javascript
微信小程序获取用户信息的两种方法wx.getUserInfo与open-data实例分析
May 03 Javascript
js实现从右往左匀速显示图片(无缝轮播)
Jun 29 Javascript
vue任意关系组件通信与跨组件监听状态vue-communication
Oct 18 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+mysql写的简单留言本实例代码
2008/07/25 PHP
简单实现限定phpmyadmin访问ip的方法
2013/03/05 PHP
php笔记之:php数组相关函数的使用
2013/04/26 PHP
php获取操作系统语言代码
2013/11/04 PHP
PHP单例模式实例分析【防继承,防克隆操作】
2019/05/22 PHP
Javascript的构造函数和constructor属性
2010/01/09 Javascript
国外大牛IE版本检测!现在IE都到9了,IE检测代码
2012/01/04 Javascript
jQuery之折叠面板的深入解析
2013/06/19 Javascript
解析Jquery中如何把一段html代码动态写入到DIV中(实例说明)
2013/07/09 Javascript
Jquery通过Ajax访问XML数据的小例子
2013/11/18 Javascript
ajaxFileUpload.js插件支持多文件上传的方法
2014/09/02 Javascript
javascript实现验证IP地址等相关信息代码
2015/05/10 Javascript
javascript实现自动输出文本(打字特效)
2015/08/27 Javascript
Bootstrap嵌入jqGrid,使你的table牛逼起来
2016/05/05 Javascript
Java框架SSH结合Easyui控件实现省市县三级联动示例解析
2016/06/12 Javascript
详解Javascript中DOM的范围
2017/02/13 Javascript
vue双花括号的使用方法 附练习题
2017/11/07 Javascript
Vue SPA单页应用首屏优化实践
2018/06/28 Javascript
vue2.0+vue-router构建一个简单的列表页的示例代码
2019/02/13 Javascript
vue-router 起步步骤详解
2019/03/26 Javascript
Handtrack.js库实现实时监测手部运动(推荐)
2021/02/08 Javascript
python实现对输入的密文加密
2019/03/20 Python
python中提高pip install速度
2020/02/14 Python
通俗讲解python 装饰器
2020/09/07 Python
Python+Xlwings 删除Excel的行和列
2020/12/19 Python
毕业生幼师求职自荐信
2013/10/01 职场文书
毕业自我鉴定范文
2013/11/06 职场文书
中专生自我鉴定范文
2013/12/19 职场文书
劳动模范事迹材料
2014/01/19 职场文书
公司拓展活动方案
2014/02/13 职场文书
人力资源部经理岗位职责规定
2014/02/23 职场文书
2014年五一劳动节社区活动总结
2014/04/14 职场文书
试用期员工工作自我评价
2014/09/10 职场文书
2014年出纳工作总结与计划
2014/12/09 职场文书
书法社团活动总结
2015/05/07 职场文书
教师工作证明范本
2015/06/12 职场文书