mobile-upload-demo icon indicating copy to clipboard operation
mobile-upload-demo copied to clipboard

移动端 Web 传图

Open progrape opened this issue 9 years ago • 14 comments

背景

WeUI QQ 交流群一直有人咨询,微信内的 Web 应用或者普通的移动端 Web 应用,如何上传图片?这里做个简要总结。

传图方式

在浏览器端,传图/传文件的方式,就是 HTML 的 <input type="file" /> 控件,而如果你的 Web 应用是特定运行在微信内的,那么还可以选择微信 App 提供的、可以通过 JSSDK 调用的传图功能。

JSSDK 传图

传图过程可以分为:

  1. 从相册中选择图片或者调起拍照功能
  2. 生成预览图
  3. 传到后端
  4. 得到传图的结果

选择图片,通过调用 chooseImage 接口实现:

wx.chooseImage({
    count: 1,
    sizeType: ['original', 'compressed'],
    sourceType: ['album', 'camera'],
    success: function (res) {
        // 返回选定照片的本地ID列表,localId可以作为img标签的src属性显示图片
        var localIds = res.localIds; 
    }
});

选择完图片后,会得到图片的标识,因为可能有多张图片,所以是个 ID 列表,需要遍历出来。该 localId 可以当成 img 标签的 src 属性使用。

也就是说,如果需要生成预览图,只需要创建 img 标签,把 localId 塞进 src 属性就可以显示出来了。例如:

var localIds = res.localIds;
for(var i = 0; i < localIds.length; i++) {
    var localId = locals[i];
    var img = new Image();
    img.src = localId;
    // 塞到 DOM 结构
    // ...
}

这样就可以在页面中预览选中的图片了。接下来就是把图片传到后端,触发条件可以是选完图马上自动传,也可以等用户点击按钮触发上传。

wx.uploadImage({
    localId: '123abc234abc3452abc',
    isShowProgressTips: 1,
    success: function (res) {
        // 图片传成功到微信服务器,得到一个 id,可以通过这个 id 在后端从微信服务器把图片下载下来
        var serverId = res.serverId;
    }
});

具体细节请参考 JSSDK 文档

input 传图

然后来看看通用的 <input type="file" /> 方式传图,其实说是通用,在 Android 4.4.1 和 Android 4.4.2 的版本,把这个功能给去掉了,点击没反应,无法调起选择图片的弹框。这个问题如果要解决,需要客户端的配合,具体方案可以网上搜索。

所幸的是,在微信 Android 客户端,使用的是 X5 内核,X5 帮忙填补了这个坑,如果你的应用只是在微信内使用,那么不需要考虑这个问题。

首先,页面中放置一个 input 控件(这里只探讨功能实现,不关注 UI,UI 层面可以使用 WeUI 的组件):

<input type="file" id="file" />

然后用户点击这个控件,就可以调起选图或者拍照了。那么如何得到选择的图片呢?如何生成预览图呢?这需要监听这个控件的 change 事件,然后使用 FileReader 对象来读:

document.getElementById('file').addEventListener('change', function (e) {
    var file = e.target.files[0];
    if (file && /^image\//i.test(file.type)) {

        var reader = new FileReader();

        reader.onloadend = function () {
            // 图片的 base64 格式, 可以直接当成 img 的 src 属性值
            var dataURL = reader.result;
            var img = new Image();
            img.src = dataURL;
            // 插入到 DOM 中预览
            // ...
        };

        // 读出base64格式
        reader.readAsDataURL(file);
    }
}, false);

读取图片,生成预览图完成了,那么怎样进行上传呢?这里有两种方式进行上传,第一种是直接向后端 post 提交 base64 格式的数据;第二种是构造 blob 二进制格式文件,以传文件的形式上传。个人非常推荐第二种方式。

提交 base64

这种方式,就是把图片当成字符串进行提交,不过缺点就是提交的字符串过大,很可能会被后端拒绝,nginx、PHP 都有限制默认提交数据大小的。

$.post('/upload', {data: dataURL}).success(function (res){
    // 拿到提交的结果
}).error(function (err){
    console.error(err);
});
提交二进制文件

推荐使用这种方式,图片很大都可以传。在图片预览阶段,我们拿到的是 base64 格式的数据,这里可以转成:

/**
 * dataURL to blob, ref to https://gist.github.com/fupslot/5015897
 * @param dataURI
 * @returns {Blob}
 */
function dataURItoBlob(dataURI) {
    var byteString = atob(dataURI.split(',')[1]);
    var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
    var ab = new ArrayBuffer(byteString.length);
    var ia = new Uint8Array(ab);
    for (var i = 0; i < byteString.length; i++) {
        ia[i] = byteString.charCodeAt(i);
    }
    return new Blob([ab], {type: mimeString});
}

然后构造 FormData 对象,填充二进制文件数据,通过 ajax 的方式进行提交:

var fd = new FormData();
var blob = dataURItoBlob(dataURL);
fd.append('file', blob);

$.ajax({
    type: 'POST',
    url: '/upload',
    data: fd,
    processData: false,
    contentType: false,
    xhr: function() {
        var xhr = new window.XMLHttpRequest();
        xhr.upload.addEventListener("progress", function(evt) {
            if (evt.lengthComputable) {
                var percentComplete = evt.loaded / evt.total;
                console.log('进度', percentComplete);
            }
        }, false);

        return xhr;
    }
}).success(function (res) {
    // 拿到提交的结果
}).error(function (err) {
    console.error(err);
});

注意:使用 jquery 的方法,不要漏了指定 processDatacontentTypefalse

这样就把数据提交到后端了。在 Chrome 的 Nework 面板,可以看到请求带有图片数据。

image

接下来就是后端接收了,这里以 Node.js 的接收为例子:

const koa = require('koa');
const mount = require('koa-mount');
const parse = require('co-busboy');
const fs = require('fs');
const path = require('path');

const app = koa();

app.use(mount('/upload', function *(next) {

    if ('POST' !== this.method) {
        return yield next;
    }

    const parts = parse(this);
    var part;

    while (part = yield parts) {
        // 保存到 upload 目录, 以当前的时间戳为文件名
        const ext = path.extname(part.filename) || '.png';
        const filename = path.join(__dirname, 'upload', new Date().getTime() + ext);
        const stream = fs.createWriteStream(filename);
        part.pipe(stream);
        console.log('uploading %s -> %s', part.filename, stream.path);
    }

    this.body = {
        ret: 0,
        msg: 'ok'
    };
}));

app.listen(3000, function (){
    console.log('listening on port 3000');
});

image

最终传图功能完成。

图片压缩上传

在 PC 端,大部分图片都会在 1M 大小以内,而且 PC 端不用太在意流量问题,所以通常不用考虑压缩的问题。而在移动端,如果用户使用的是移动网络,即使你不考虑网速,也要考虑一下流量问题。

下面来看看移动端 Web 环境下如何在上传前压缩图片。

回顾一下刚才选择完图片后生成预览图的步骤:

document.getElementById('file').addEventListener('change', function (e) {
    var file = e.target.files[0];
    if (file && /^image\//i.test(file.type)) {

        var reader = new FileReader();

        reader.onloadend = function () {
            // 图片的 base64 格式, 可以直接当成 img 的 src 属性值
            var dataURL = reader.result;
            var img = new Image();
            img.src = dataURL;
            // 插入到 DOM 中预览
            // ...
        };

        // 读出base64格式
        reader.readAsDataURL(file);
    }
}, false);

reader.onloadend 回调方法里面,我们可以读到图片的 base64 格式数据,也 new 了一张图片,怎样进行图片压缩?这时候我们要请出 canvas。先来整理一下思路:

  1. new 出来的 Image 对象,我们监听它的 onload 事件
  2. 按照压缩比例,算出压缩后的图片尺寸
  3. 创建 canvas ,尺寸设置成上一步骤算出来的压缩后的图片尺寸
  4. 调用 drawImage 方法,把图片绘制到 canvas
  5. 调用 canvas 的 toDataURL ,取出 base64 格式的数据
  6. 后续的传图步骤和上面的原图上传一样

代码如下:

var img = new Image();

img.onload = function () {
    // 当图片宽度超过 400px 时, 就压缩成 400px, 高度按比例计算
    // 压缩质量可以根据实际情况调整
    var w = Math.min(400, img.width);
    var h = img.height * (w / img.width);
    var canvas = document.createElement('canvas');
    var ctx = canvas.getContext('2d');

    // 设置 canvas 的宽度和高度
    canvas.width = w;
    canvas.height = h;

    // 把图片绘制到 canvas 中
    ctx.drawImage(img, 0, 0, w, h);

    // 取出 base64 格式数据
    var dataURL = canvas.toDataURL('image/png');

    // ...
};

img.src = reader.result;

需要注意的是,通过 canvas 绘制的图片,低版本 iOS 会出现比例不正确的情况,请参考 https://github.com/stomita/ios-imagefile-megapixel ,本文不做探讨。

progrape avatar May 13 '16 12:05 progrape

有一个问题是,在安卓下不能拍照,只能从图库中选择。iOS 下是可以拍照或者选图的。

horizon0514 avatar May 14 '16 09:05 horizon0514

有没有上传附件,视频,文档的啊?

fangmingcong avatar May 17 '16 07:05 fangmingcong

上传的图片角度不对,如何解决?

fangmingcong avatar May 18 '16 09:05 fangmingcong

旋转问题参考 https://github.com/exif-js/exif-js

progrape avatar May 18 '16 09:05 progrape

有上传附件的插件吗?谢谢

fangmingcong avatar May 18 '16 09:05 fangmingcong

旋转问题,没看懂哦,可以教下我吗?

fangmingcong avatar May 18 '16 09:05 fangmingcong

Android 可以选择文件,iOS 只能选图片

progrape avatar May 18 '16 09:05 progrape

现在这个插件,只能选择图片吧。

fangmingcong avatar May 18 '16 09:05 fangmingcong

iOS 不能选其他文件,是系统的限制,不是这个插件的限制

progrape avatar May 18 '16 11:05 progrape

这个我知道,我现在也要这个插件,进行上传视频,word,我应该怎么改

fangmingcong avatar May 19 '16 01:05 fangmingcong

这个图片压缩的 压缩前,怎么比压缩后还大?这个我打印出来 压缩前 246.86328125 压缩后 576.4697265625

fangmingcong avatar Jun 24 '16 02:06 fangmingcong

这个不错啊!这个我想在增加这个表单是怎么弄呢?

msmax avatar Jan 04 '17 03:01 msmax

选择后有删除的方法吗,大家都是自己实现?

coderws avatar Jul 20 '17 01:07 coderws

用canvas生成图片在手机端无法显示,怎么办

xiayedingdan avatar Aug 22 '17 08:08 xiayedingdan