Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

移动端 Web 传图 #1

Open
progrape opened this issue May 13, 2016 · 14 comments
Open

移动端 Web 传图 #1

progrape opened this issue May 13, 2016 · 14 comments

Comments

@progrape
Copy link
Owner

progrape commented May 13, 2016

背景

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 ,本文不做探讨。

@horizon0514
Copy link

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

@fangmingcong
Copy link

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

@fangmingcong
Copy link

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

@progrape
Copy link
Owner Author

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

@fangmingcong
Copy link

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

@fangmingcong
Copy link

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

@progrape
Copy link
Owner Author

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

@fangmingcong
Copy link

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

@progrape
Copy link
Owner Author

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

@fangmingcong
Copy link

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

@fangmingcong
Copy link

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

@msmax
Copy link

msmax commented Jan 4, 2017

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

@coderws
Copy link

coderws commented Jul 20, 2017

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

@xiayedingdan
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants