文档章节

前端对图片进行本地压缩预览并上传实践

oj8kay
 oj8kay
发布于 2017/09/11 10:22
字数 2343
阅读 1561
收藏 200
点赞 13
评论 10

相信做过前端的小伙伴们都写过图片上传,最简单的方式是通过表单提交,一个<input type="file">加一个<button type="submit">,外面包一层form就搞定了。(button默认的type是submit,这里可以省略,之所以特别写出来,是因为以前碰到过form标签里面写了一个<button>标签,一点击就莫名奇妙地把表单提交了,希望大家引以为戒

但是表单提交会刷新整个页面,于是有心的朋友为了做无刷新的表单提交,可能就会搞一个隐藏的iframe,然后将form标签的target指向这个iframe,这样用户就感知不到页面的刷新。

更细心点的朋友可能会在<input type="file">加上一个accept="image/*"属性,使文件选择框只能选择图片,不过如果你在chrome上这样写可能会碰到窗口打开非常慢的情况,并不是电脑卡,根本原因是当打开文件选择器时,chrome会访问谷歌服务器来拉取图片的mime-type,但是因为天朝的科学上网机制,谷歌当然是访问不了的,所以会有几秒连接超时前的卡顿,如果你有速度够快的梯子,应该可以避免这种情况。比较适应国情的方法是直接在accept里面写上你需要的mime-type,只要是在chrome的白名单里面,就不会去访问谷歌啦,比如:

<input type="file" name="imgFile" accept="image/png, image/jpeg, image/gif">

不过之前测试碰到一个问题,三星手机上如果这么写

<input type="file" name="imgFile" accept="image/png, image/jpeg, image/gif" capture="camera">

是无法调起手机照相机的。还必须得accept="image/*"才行。

如果公司的前端项目打算上html5了,或许拥抱xhr2是更好的选择。xhr2即XMLHttpRequest Level 2,新的规范相较xhr的初版增加了许多有用的新特性,本次实践主要用到FormData和Blob接口。

简单来说,图片预览、压缩和上传主要分这么几步:

  1. 给<input type="file">添加onchange事件,在事件回调中获取元素的files属性;
  2. 创建Image对象,并添加onload事件回调;
  3. 把File对象(File继承Blob)转化为blob url,并赋给Image对象的src属性
  4. 在Image对象的onload回调中创建canvas画布,并将图片写入画布
  5. 通过canvas对象的toDataURL方法,以指定的输出质量生成data url(本质是base64字符串)
  6. 有了base64,我们就可以通过一定的算法将其还原为二进制对象(Blob对象),或者通过canvas的toBlob来输出blob
  7. 最后将blob对象append进FormData,通过ajax来post到服务器即可

 

觉得so easy?咱们增加点难度,要求代码可以指定图片最终压缩后的大小以及尺寸(锁定宽高比),okay,下面来实际操作一下,整个项目的结构非常简单:

uploads是文件上传目录,public是静态资源目录

首先用node express搭建一个的服务器,Multer作为处理 multipart/form-data 的中间件,后端的代码一共就这么点,我就不多说了:

express.js:

const express = require('express')
const multer = require('multer')
var storage = multer.diskStorage({
    destination: function (req, file, cb) {
        cb(null, './uploads')
    },
    filename: function (req, file, cb) {
        cb(null, Date.now() + '-' + file.originalname)
    }
})
const upload = multer({ storage: storage })

const app = express()

app.get('/file/:name', function (req, res, next) {
    res.sendFile(req.params.name, { root: __dirname + '/public/' })
})


app.post('/upload', upload.single('avatar'), function (req, res, next) {
    res.json({ msg: 'upload over' })
});

const server = app.listen(3000, function () {
    const host = server.address().address
    const port = server.address().port
    console.log('server listening at %s:%s', host, port)
})

接着写我们的前端代码,先创建一个简单的html页面

test.html:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
</head>

<body>
    <input type="file" name="file" accept="image/png, image/jpeg, image/jpg" id="file">
    <img src="" alt="" id="preview">
    <button type="button" id="upload">上传</button>
    <script>

        const fileEle = document.getElementById('file')
        const uploadEle = document.getElementById('upload')
        const previewEle = document.getElementById('preview')

        let imgFile = null

        function compress(target, quality_size, maxWidth, maxHeight, onSuccess) {
            // 这里做压缩
        }

        fileEle.onchange = function (e) {
            // 这里调用compress函数
        }
        uploadEle.onclick = function (e) {
            // 点击上传图片
        }
    </script>
</body>

</html>

然后在onchange回调中获取file

fileEle.onchange = function (e) {
    // 这里调用compress函数
    if (fileEle.files.length > 0) {
        const file = e.target.files[0]
        compress(file, 500, 1000, 1000, function (data) {
            previewEle.src = data.dataUrl
            imgFile = data.blob
        })
    }
}

compress是我们正在处理压缩的方法,它接收五个参数:需要压缩的文件,最终压缩的文件大小(KB),最大宽度,最大高度。

接下来编写compress函数进行压缩操作。

let _ctx, _mimeType, _width, _height, _quality, _targetSize, _onSuccess
/**
* 压缩图片文件
* @param target 图片文件
* @param quality_size 文件大小
* @param maxWidth  最大宽度
* @param maxHeight 最大高度
* @param onSuccess 成功回调
*/
function compress(target, quality_size, maxWidth, maxHeight, onSuccess) {
    // 这里做压缩
    if (typeof target === 'object') {  // 首次执行
        const file = target
        const fileSize = file.size / 1000
        _targetSize = quality_size
        _mimeType = file.type
        _onSuccess = onSuccess
        const srcImg = new Image()
        const srcImgData = URL.createObjectURL(file)
        if (fileSize < _targetSize) {
            _onSuccess({ dataUrl: srcImgData, blob: file })
            return false
        }
        srcImg.src = srcImgData
        srcImg.onload = function () {
            _width = srcImg.naturalWidth
            _height = srcImg.naturalHeight
            if (_width > maxWidth) {
                _height = maxWidth / _width * _height
                _width = maxWidth
            }
            if (_height > maxHeight) {
                _width = maxHeight / _height * _width
                _height = maxHeight
            }
            const ratio = _width / _height
            _cvs = document.createElement('canvas');
            _cvs.width = _width;
            _cvs.height = _height;
            _ctx = _cvs.getContext("2d")
            _ctx.drawImage(srcImg, 0, 0, _width, _height)
            var imgData = _cvs.toDataURL(_mimeType, 1);
            const imgSize = Math.round(imgData.replace('data:' + _mimeType + ';base64,', '').length * 3 / 4) / 1000
            imgSize > _targetSize ? compress(imgData, _targetSize / imgSize) : _cvs.toBlob(blob => { _onSuccess({ dataUrl: imgData, blob: blob }) }, _mimeType, quality)
        }
    } else if (typeof target === 'string') {
        const imgData = target
        const newImg = new Image()
        const quality = quality_size > 0.9 ? 0.9 : quality_size
        newImg.src = imgData
        newImg.onload = function () {
            _ctx.clearRect(0, 0, _width, _height)
            _ctx.drawImage(newImg, 0, 0);
            var newImgData = _cvs.toDataURL(_mimeType, quality);
            const newImgSize = Math.round(newImgData.replace('data:' + _mimeType + ';base64,', '').length * 3 / 4) / 1000
            if (newImgSize > _targetSize) {
                compress(newImgData, _targetSize / newImgSize)
            } else {
                _cvs.toBlob(blob => { _onSuccess({ dataUrl: newImgData, blob: blob }) }, _mimeType, quality)
            }
        }
    }
}

之所以先判断target类型是因为首次调用compress时传入的是一个file对象,后面进行递归的时候传入的是dataUrl字符串。函数比较关键的几步需要注意:

① 在递归前我会先去掉imgData的描述头

imgData.replace('data:' + _mimeType + ';base64,', '')

就可以获取base64字符串,根据base64的生成算法(见维基),一个字符代表6位,于是获取base64字符串的长度,然后除6乘8就可以得到这个字符串还原为二进制后的字节数,这就是下面这行代码的原理:

const imgSize = Math.round(imgData.replace('data:' + _mimeType + ';base64,', '').length * 3 / 4) / 1000

② 虽然知道了base64字符串所代表的二进制对象大小,但是要将文件上传,还是需要转化为真正的二进制Blob对象(另一种做法是直接提交base64字符串,后台对其进行转换,这么做的坏处是上传的数据量增大了 1/3)。Blob的构造函数可以接受一个类型数组,通过这个方式我们就可以将base64字符串还原为二进制对象,如果图方便,可以直接使用canvas的toBlob方法来直接生成Blob,不过ios似乎暂时并不支持原生的toBlob,这里可以使用MDN提供的基于toDataURL实现的polyfill:

if (!HTMLCanvasElement.prototype.toBlob) {
 Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
  value: function (callback, type, quality) {

    var binStr = atob( this.toDataURL(type, quality).split(',')[1] ),
        len = binStr.length,
        arr = new Uint8Array(len);
    for (var i = 0; i < len; i++ ) {
     arr[i] = binStr.charCodeAt(i);
    }
    callback( new Blob( [arr], {type: type || 'image/png'} ) );
  }
 });
}

③ 上面的polyfill用到了window.atob这个js原生方法,其作用是解码一个已经被base-64编码过的数据,相对应的还有一个window.btoa函数可以将ascii字符串或二进制数据转换成一个base64编码过的字符串,该方法不能直接作用于Unicode字符串(原因及解决办法见MDN的API文档),目前两个方法也都有兼容性问题,可以通过另一个polyfill来解决:

/**
    * Base64编/解码
    * @type {{characters: string, encode: Base64.encode, decode: Base64.decode}}
    */
let Base64 = {
    characters: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=',
    /**
        * 对字符串编码
        * @param {string} string
        * @returns {string}
        */
    encode: function (string) {
        let characters = Base64.characters
        let result = ''

        let i = 0
        do {
            let a = string.charCodeAt(i++)
            let b = string.charCodeAt(i++)
            let c = string.charCodeAt(i++)

            a = a || 0
            b = b || 0
            c = c || 0

            let b1 = (a >> 2) & 0x3F
            let b2 = ((a & 0x3) << 4) | ((b >> 4) & 0xF)
            let b3 = ((b & 0xF) << 2) | ((c >> 6) & 0x3)
            let b4 = c & 0x3F

            if (!b) {
                b3 = b4 = 64
            } else if (!c) {
                b4 = 64
            }

            result += characters.charAt(b1) + characters.charAt(b2) + characters.charAt(b3) + characters.charAt(b4)

        } while (i < string.length)

        return result
    },

    /**
        * 对base64字符串解码
        * @param {string} string
        * @returns {string}
        */
    decode: function (string) {
        let characters = Base64.characters
        let result = ''

        let i = 0
        do {
            let b1 = characters.indexOf(string.charAt(i++))
            let b2 = characters.indexOf(string.charAt(i++))
            let b3 = characters.indexOf(string.charAt(i++))
            let b4 = characters.indexOf(string.charAt(i++))

            let a = ((b1 & 0x3F) << 2) | ((b2 >> 4) & 0x3)
            let b = ((b2 & 0xF) << 4) | ((b3 >> 2) & 0xF)
            let c = ((b3 & 0x3) << 6) | (b4 & 0x3F)

            result += String.fromCharCode(a) + (b ? String.fromCharCode(b) : '') + (c ? String.fromCharCode(c) : '')

        } while (i < string.length)

        return result
    }
}
window.btoa = Base64.encode
window.atob = Base64.decode

④ 本人对图片压缩算法并不了解,canvas的toDataURL方法指定输出质量原理是啥也不清楚,所以我用了一个比较“2”的办法,循环压缩图片,直到输出大小符合我们的要求——这个做法似乎十分蹩脚,如果大佬有更好的方法望指教。

最后就可以将我们的图片上传了:

uploadEle.onclick = function (e) {
    // 点击上传图片
    if (fileEle.files.length > 0) {
        const formData = new FormData()
        console.log(fileEle.files[0])
        imgFile && formData.append('avatar', imgFile, 'avatar.' + _mimeType.split('/')[1])
        formData.append('user', '666666')
        $.ajax({
            type: 'post',
            url: '/upload',
            data: formData,
            contentType: false,
            processData: false,
            success: function (res) {
                console.log(res)
            },
            dataType: 'json'
        })
    }
}

FormData.append方法的第一个参数是fieldName(字段/参数名),第二个是待提交的数据,第三个是originalName(文件名),案例图简单,就直接使用jQuery了。

完整代码戳这里

© 著作权归作者所有

共有 人打赏支持
oj8kay

oj8kay

粉丝 22
博文 17
码字总数 33329
作品 0
杭州
前端工程师
加载中

评论(10)

双曲线

引用来自“中柠檬”的评论

写得不错

引用来自“GaiSama”的评论

那不点个赞嘛:laughing:
不止点赞,还搜藏了
oj8kay
oj8kay

引用来自“OSC_BmJpzF”的评论

表示已经放弃base64上传图片,部分手机获取不到
能提供一下手机型号和操作系统嘛?我测试了部分机型,除了性能堪忧外没碰到其他问题
高久峰
高久峰
表示已经放弃base64上传图片,部分手机获取不到
oj8kay
oj8kay

引用来自“孤单的不同世界”的评论

gzip
gzip编码跟这篇文章的压缩似乎不是一回事。。:sweat_smile:
孤单的不同世界
孤单的不同世界
gzip
土豆哥哥好
土豆哥哥好
收藏了
oj8kay
oj8kay

引用来自“蓝水晶飞机”的评论

canvas 有一个可以指定图片输出压缩质量的数值参数吧!
是的,这里的压缩就是基于这个实现的
蓝水晶飞机
蓝水晶飞机
canvas 有一个可以指定图片输出压缩质量的数值参数吧!
oj8kay
oj8kay

引用来自“中柠檬”的评论

写得不错
那不点个赞嘛:laughing:
爱吃柠檬的橘子
爱吃柠檬的橘子
写得不错
KodExplorer 4.06 发布,全面开放,支持完整插件模式

KodExplorer 4.06 发布了。KodExplorer可 道云,原名芒果云,是基于 Web 技术的私有云和在线文件管理系统,它提供了类windows经典用户界面,一整套在线文件管理、文件预览、编辑、上传下载、...

雾渺 ⋅ 2017/09/01 ⋅ 10

无需Flash实现图片裁剪——HTML5中级进阶

前言 图片裁剪上传,不仅是一个很贴合用户体验的功能,还能够统一特定图片尺寸,优化网站排版,一箭双雕。 需求就是那么简单,在浏览器里裁剪图片并上传到服务器。 我第一个想到的方法就是,...

力谱宿云 ⋅ 2016/05/09 ⋅ 0

weui上传文件完整例子,后台Java接受,SSM框架,要解决的问题是接受2张以上图片

weUI的Uploader组件,目前已实现的功能是,一个input type=file,上传图片,有压缩预览,最多6张,每读取一张,js就添加了节点预览一张,但是表单直接提交时,后台获得的是文件标签最后一次操...

辉煌霸猪 ⋅ 2017/11/30 ⋅ 2

KodExplorer 4.22 发布,拖拽增强,安全性优化

KodExplorer 4.22 发布了。KodExplorer可 道云,原名芒果云,是基于 Web 技术的私有云和在线文件管理系统,它提供了类windows经典用户界面,一整套在线文件管理、文件预览、编辑、上传下载、...

雾渺 ⋅ 2017/10/02 ⋅ 5

可道云在线资源管理器 - kodExplorer

KodExplorer可道云,原名芒果云,是基于Web技术的私有云和在线文件管理系统。致力于为用户提供安全可控、可靠易用、高扩展性的私有云解决方案。用户只需通过简单环境搭建,即可使用KodExplor...

雾渺 ⋅ 2013/09/07 ⋅ 53

Jcrop 整合 FileAPI 图像裁剪上传

Jcrop是一款优秀的jQuery插件,可以非常方便地实现图像裁剪,而且功能十分的强大。 一般的情况下,图像裁剪的实现要经过两次图像上传,第一次将图片上传到后台,后台返回一个链接,通过这个链...

Acce1erator ⋅ 2015/12/28 ⋅ 0

文件上传组件--WebUploader

WebUploader 是由Baidu WebFE(FEX)团队开发的一个简单的以HTML5为主,FLASH为辅的现代文件上传组件。在现代的浏览器里面能充分发挥HTML5的优势,同时又不摒弃主流IE浏览器,延用原来的FLASH...

缪斯的情人 ⋅ 2014/07/15 ⋅ 12

KodExplorer 3.45 发布,在线解压缩,压缩文件预览支持

KodExplorer 3.45 发布了。KodExplorer是款开源的Web在线文件管理、代码编辑器。它提供了类windows经典用户界面,一整套在线文件管理、文件预览、编辑、上传下载、在线解压缩、音乐播放功能。...

雾渺 ⋅ 2017/04/14 ⋅ 16

WebUploader的缩略图如何在后台保存以及下次打开页面时如何展示的问题

大家好,我的使用场景如下:(我觉得比较通用) 用户在需要专家帮忙的时候需要填写文字信息以及上传图片 可上传多图,每选择一个图片无需点击“上传”按钮而自动上传 上传成功后,出现缩略图...

错觉 ⋅ 2015/12/09 ⋅ 6

HTML5 进阶系列:文件上传下载

前言 HTML5 中提供的文件API在前端中有着丰富的应用,上传、下载、读取内容等在日常的交互中很常见。而且在各个浏览器的兼容也比较好,包括移动端,除了 IE 只支持 IE10 以上的版本。想要更好...

林鑫 ⋅ 2017/07/03 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

面试-JVM 内存结构

JVM 内存结构

秋日芒草 ⋅ 1分钟前 ⋅ 0

马氏距离与欧氏距离

马氏距离 马氏距离也可以定义为两个服从同一分布并且其协方差矩阵为Σ的随机变量之间的差异程度。 如果协方差矩阵为单位矩阵,那么马氏距离就简化为欧氏距离,如果协方差矩阵为对角阵,则其也...

漫步当下 ⋅ 24分钟前 ⋅ 0

聊聊spring cloud的RequestRateLimiterGatewayFilter

序 本文主要研究一下spring cloud的RequestRateLimiterGatewayFilter GatewayAutoConfiguration @Configuration@ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMi......

go4it ⋅ 56分钟前 ⋅ 0

Spring JavaConfig 注解

JavaConfig注解允许开发者将Bean的定义和配置放在Java类中。它是除使用XML文件定义和配置Bean外的另一种方案。 配置: 如一个Bean如果在XML文件可以这样配置: <bean id="helloBean" class="...

霍淇滨 ⋅ 今天 ⋅ 0

Spring clound 组件

Spring Cloud技术应用从场景上可以分为两大类:润物无声类和独挑大梁类。 润物无声,融合在每个微服务中、依赖其它组件并为其提供服务。 Ribbon,客户端负载均衡,特性有区域亲和、重试机制。...

英雄有梦没死就别停 ⋅ 今天 ⋅ 0

Confluence 6 重新获得站点备份文件

Confluence 将会创建备份,同时压缩 XML 文件后存储熬你的 <home-directory>/backups> 目录中。你需要自己访问你安装的 Confluence 服务器,并且从服务器上获得这个文件。 运行从 Confluence...

honeymose ⋅ 今天 ⋅ 0

informix的常用SQL语句

1、创建数据库 eg1. 创建不记录日志的库testdb,参考语句如下: CREATE DATABASE testdb; eg2. 创建带缓冲式的记录日志的数据库testdb(SQL语句不一定在事务之中,拥有者名字不被用于对象的解...

wangxuwei ⋅ 今天 ⋅ 0

matplotlib画图

最简单的入门是从类 MATLAB API 开始,它被设计成兼容 MATLAB 绘图函数。 from pylab import *from numpy import *x = linspace(0, 5, 10)y = x ** 2figure()plot(x, y, 'r')...

Dr_hu ⋅ 今天 ⋅ 0

RabbitMQ学习以及与Spring的集成(三)

本文介绍RabbitMQ与Spring的简单集成以及消息的发送和接收。 在RabbitMQ的Spring配置文件中,首先需要增加命名空间。 xmlns:rabbit="http://www.springframework.org/schema/rabbit" 其次是模...

onedotdot ⋅ 今天 ⋅ 0

JAVA实现仿微信红包分配规则

最近过年发红包拜年成为一种新的潮流,作为程序猿对算法的好奇远远要大于对红包的好奇,这里介绍一种自己想到的一种随机红包分配策略,还请大家多多指教。 算法介绍 一、红包金额限制 对于微...

小致dad ⋅ 今天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部