scribble

Leon's scribble.

About Email GitHub

18 Nov 2015
HTML5 FileReader 实现图片预览和上传

回顾一下以前的图片上传方式

在 HTML5 出现之前,通常是使用 Form + Input file 来上传图片,虽然在之后衍生出了 Ajax 异步提交的黑科技,但本质上还是隐藏 Iframe + Form,这种方法通过监听 Iframe 的 readystate 状态 改变来处理上传进度和结果,这种方式相比纯 Form 提交的方式在体验上得到了很大的提升,因为不用刷新页面了,很多单页面应用都采用的这种方式。

使用 HTML5 FileReader 上传

HTML5 出现了,图片上传的花样就多了,XMLHttpRequest Level 2 有一个 FormData 接口,也可以用来异步上传二进制文件,这篇文章 作了详细的解读。

好了,现在来看看如何用 FileReader 来实现上面说的目的,原理其实就是通过 FileReader API 读取本地的图片文件,然后将文件转换成 base64 编码的字符串,即 Data URL, 说到这里,假如你对 Canvas 有一定了解,可能很快就明白了实现过程。

是的,最终目的是得到 Data URL 编码串,最后将其提交到后台,后台再转换为二进制图片文件。

下面是实现过程:

HTML:

<input type="file" id="filechooser" />
<img alt="Image Previewer" id="previewer">

对于浏览器默认的上传控件实在太丑这个问题,相信你也注意到了,所以在实际项目中,通常我们都要自定义一个漂亮的上传按钮,然后监听其点击并触发默认控件的 click 事件。

JS:

var filechooser = document.getElementById('filechooser');
var previewer = document.getElementById('previewer');

filechooser.onchange = function() {
    var files = this.files;
    var file = files[0];

    // 接受 jpeg, jpg, png 类型的图片
    if (!/\/(?:jpeg|jpg|png)/i.test(file.type)) return;

    var reader = new FileReader();

    reader.onload = function() {
        var result = this.result;

        previewer.src = result;

        // 清空图片上传框的值
        filechooser.value = '';
    };

    reader.readAsDataURL(file);
};

上面代码中,主要用到了 FileReader 的 readAsDataURL 实例方法,用来将 File 对象转换成 Data URL,这样就完成了图片预览功能。

另外,FileReader 还有一个 readAsText 方法,可以读取文本文件中的内容,还可以指定编码方式,用处也很大。

结合 canvas 使用

为什么要结合 canvas 使用呢,canvas 有一个 toDataURL 方法,可以设置图片质量,利用这一特性,就可以用来压缩图片,还是基于上面的代码,为图片上传增加一个压缩功能,比如超过 200KB,就按 75% 来压缩图片。

关于 canvas 压缩图片,也可以参考我之前的文章 HTML5 CANVAS 实现图片压缩和裁切

var filechooser = document.getElementById('filechooser');
var previewer = document.getElementById('previewer');

// 200 KB 对应的字节数
var maxsize = 200 * 1024;

filechooser.onchange = function() {
    var files = this.files;
    var file = files[0];

    // 接受 jpeg, jpg, png 类型的图片
    if (!/\/(?:jpeg|jpg|png)/i.test(file.type)) return;

    var reader = new FileReader();
    reader.onload = function() {
        var result = this.result;
        var img = new Image();

        // 如果图片小于 200kb,不压缩
        if (result.length <= maxsize) {
            toPreviewer(result);
            return;
        }

        img.onload = function() {
            var compressedDataUrl = compress(img, file.type);
            toPreviewer(compressedDataUrl);
            img = null;
        };

        img.src = result;
    };

    reader.readAsDataURL(file);
};

function toPreviewer(dataUrl) {
    previewer.src = dataUrl;
    filechooser.value = '';
}

function compress(img, fileType) {
    var canvas = document.createElement("canvas");
    var ctx = canvas.getContext('2d');

    var width = img.width;
    var height = img.height;

    canvas.width = width;
    canvas.height = height;

    ctx.fillStyle = "#fff";
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.drawImage(img, 0, 0, width, height);

    // 压缩
    var base64data = canvas.toDataURL(fileType, 0.75);
    canvas = ctx = null;

    return base64data;
}

查看 Demo

移动端使用的问题

项目使用中发现在 PC 上一切正常,但在 IOS 8.2 和 IOS 7.1.2 系统上有个问题,选择手机拍摄的图片在经过 canvas 压缩后,Data URL 变成了一长串空白像素的字符串,表现上就是预览区域是一片白, 而手机截屏的图片却能正常压缩,区别在于前者是 JPG 图片,后者是 PNG 格式的,目前尚不知原因所在,在 stackoverflow 上有这么一条记录 FileReader not working on iOS 8,但和我遇到 的不是同一个问题。由于代码主要运行在手机浏览器,最后只能先把压缩功能撤销处理,改为原样上传。

caniuse 上可以看到,filereader 支持移动端 safari 6.1+ 和安卓原生浏览器 3.0+,canvas 支持 IOS safari 3.2+ 以及安卓原生浏览器 3.0+。 从兼容性支持上来看,还是非常不错的。


参考资料


Til next time,
Leon at 12:37

scribble

About Email GitHub