15

前段时间参与了一个H5项目,里边有个需求是用户上传图片。当时的方案是前端先调用微信的JSSDK选择图片并上传,然后再从后端下载到服务器上。然而用的时候发现客户端给的图片有大有小,但是由于用了微信的接口,图片在下载之前是没法控制的。后来在想能不能调用HTML5原生的文件上传接口,另外还可以配合阿里云的OSS对图片做进一步处理,所以就有了这篇文章。

1. HTML5原生上传

其实之前也有想过用原生的,可手里的项目全是微信平台的H5,原生上传一直被告知有兼容性问题,所以这个方案一直是被搁置的;只是这次觉得用微信接口实在不爽才重新翻出来的,没想到意外发现手里的米4居然可以正常用。。好了闲话不说,上代码:

<input id="img_input" type="file" accept="image/*" />
<div id="preview_box"></div>

HTML部分主要就是那个input,至于下边那个div,主要是留着放图片预览用的。

<script src="http://cdn.bootcss.com/jquery/2.2.4/jquery.min.js"></script>
<script>
    $("#img_input").on("change", function(e) {
        var file = e.target.files[0]; // 获取图片资源
        var fd = new FormData(); // 用formdata上传文件

        // 只选择图片文件
        if (!file.type.match('image.*')) {
            return false;
        }

        fd.append('file', file, file.name); // 填入文件

        $.ajax({
            url: 'fileupload.php',
            data: fd,
            processData: false,
            contentType: false,
            type: 'POST',
            success: function () {
                // 成功后显示文件预览
                var reader = new FileReader();
                reader.readAsDataURL(file); // 读取文件
                // 渲染文件
                reader.onload = function(ev) {
                    var img = '<img class="preview" src="' + ev.target.result + '" alt="preview"/>';
                    $("#preview_box").empty().append(img);
                }
            }
        });
    });
</script>

文件填入FormData,然后POST上传,后端(用的PHP)简单写下接收就行。
(然后这里顺便想问下如果直接上传blob的话,PHP后端应该怎么写?有大神路过请不吝赐教~小弟这里先谢过了)

<?php

if ($error == UPLOAD_ERR_OK) {
    $tmp_name = $_FILES["file"]["tmp_name"];
    $name = $_FILES["file"]["name"];
    move_uploaded_file($tmp_name, "$name");
}

然后处理下权限啥的,就能跑啦。

2. 前端压缩(localResizeIMG)

localResizeIMG 是个好插件,用法也很简单,把 GitHub 里的 dist 文件夹拖下来改个名(我改了个“localRZ”),然后直接引用 lrz.bundle.js 文件就行了:

<script src="http://cdn.bootcss.com/jquery/2.2.4/jquery.min.js"></script>
<script src="localRZ/lrz.bundle.js"></script>
<script>
    $("#img_input").on("change", function(e) {
        var file = e.target.files[0]; //获取图片资源
        var filename = file.name;

        // 只选择图片文件
        if (!file.type.match('image.*')) {
            return false;
        }
        // LocalResizeIMG处理:
        lrz(file, {width: 400})
            .then(function (rst) {
                $.ajax({
                    url: 'fileupload.php',
                    data: rst.formData, // LocalResizeIMG 直接封装好的
                    processData: false,
                    contentType: false,
                    type: 'POST'
                }).done(function(data, textStatus, jqXHR){
                    // 图片预览
                    var img = new Image();
                    img.src = rst.base64;

                    img.onload = function () {
                        $("#preview_box").empty().append(img);
                    };
                });
                return rst;
            })
            .catch(function (err) {
                // 万一出错了,这里可以捕捉到错误信息
                // 而且以上的then都不会执行
                alert('ERROR:' + err);
            })
            .always(function () {
                // 不管是成功失败,这里都会执行
            });

    });
</script>

localResizeIMG 的 文档 写的挺清楚的,哪里不明白的话可以过去看看。

3. 美化上传按钮

原生的文件上传控件略丑,所以一般是要美化一下。
HTML:

<div class="filePicker">
    <input id="img_input" type="file" accept="image/*" />
    <label for="img_input">上传图片</label>
</div>

<div class="preview_box"></div>

放一个 lable 上去,然后隐藏掉原有的 input:

<style type="text/css">
    .filePicker {
        margin: 200px;
        width: 200px;
        height: 50px;
        line-height: 50px;
        text-align: center;
        color: #fff;
        background: #00b7ee;
    }
    
    .filePicker label {
        display: block;
        width: 100%;
        height: 100%;
    }
    
    .filePicker input[type="file"] {
        display: none;
    }

</style>

这样看起来就舒服多了。

4. 对接OSS

关于直传,阿里官方给了三种方案:

  1. 客户端 JavaScript 签名后直传;

  2. 客户端申请服务端签名,然后打包上传;

  3. 客户端申请服务端签名,打包上传OSS后回调服务端。

这里主要用的是第二种。

根据官方给的案例代码,首先要搞个签名用的PHP:

<?php

function gmt_iso8601($time) {
    $dtStr = date("c", $time);
    $mydatetime = new DateTime($dtStr);
    $expiration = $mydatetime->format(DateTime::ISO8601);
    $pos = strpos($expiration, '+');
    $expiration = substr($expiration, 0, $pos);
    return $expiration."Z";
}

//自行设置AccessKey和相应Bucket的外网域名
$id= 'xxxxxxxxxxx';
$key= 'yyyyyyyyyy';
$host = 'http://zzzzzzz.oss-cn-xxxxxxxxx.aliyuncs.com/';

$now = time();
$expire = 10; //设置该policy超时时间是10s. 即这个policy过了这个有效时间,将不能访问
$end = $now + $expire;
$expiration = gmt_iso8601($end);

//文件大小范围.用户可以自己设置
$condition = array(0=>'content-length-range', 1=>0, 2=>1048576000);

//设置用户上传指定的前缀
$dir = 'test/';
//用户上传数据的位置匹配,这一步不是必须项,只是为了安全起见,防止用户通过policy上传到别人的目录
$start = array(0=>'starts-with', 1=>'$key', 2=>$dir);

//设置bucket
$bucket = array(0=>'eq', 1=>'$bucket', 2=>'gmei');

$conditions = array(0=>$bucket, 1=>$condition, 2=>$start);


$arr = array('expiration'=>$expiration,'conditions'=>$conditions);
//echo json_encode($arr);
//return;
$policy = json_encode($arr);
$base64_policy = base64_encode($policy);
$signature = base64_encode(hash_hmac('sha1', $base64_policy, $key, true));

$response = array(
    'accessid' => $id,
    'host' => $host,
    'policy' => $base64_policy,
    'signature' => $signature,
    'expire' => $end,
    'dir' => $dir.'${filename}'
);

echo json_encode($response);

里边的东西填一下,然后保存在同目录下就行。然后改下HTML:

<script src="http://cdn.bootcss.com/jquery/2.2.4/jquery.min.js"></script>
<script src="localRZ/lrz.bundle.js"></script>
<script>
    $("#img_input").on("change", function(e) {
        var file = e.target.files[0]; //获取图片资源
        var filename = file.name;

        // 只选择图片文件
        if (!file.type.match('image.*')) {
            return false;
        }
        // LocalResizeIMG写法:
        lrz(file, {width: 200, fieldName: 'osstest'})
            .then(function (rst) {
                // OSS要求把上传文件放到最后一项,但是用LocalResizeIMG输出的FormData,就只能放在
                // 第一项,所以这里要自己new个出来
                var ossData = new FormData();
                // 先请求授权,然后回调
                $.getJSON('ossget.php', function (json) { //签名用的PHP
                    // 添加签名信息
                    ossData.append('OSSAccessKeyId', json.accessid);
                    ossData.append('policy', json.policy);
                    ossData.append('Signature', json.signature);
                    ossData.append('key', json.dir);
                    // 添加文件
                    ossData.append('file', rst.file, filename);

                    $.ajax({
                        url: json.host,
                        data: ossData,
                        processData: false,
                        contentType: false,
                        type: 'POST'
                    }).done(function(){
                        // 成功后显示图片预览
                        var img = new Image();
                        img.src = rst.base64;
                        img.onload = function () {
                            $(".preview_box").empty().append(img);
                        };
                    });
                });
                return rst;
            })
            .catch(function (err) {
                // 万一出错了,这里可以捕捉到错误信息
                // 而且以上的then都不会执行
                alert('ERROR:' + err);
            })
            .always(function () {
                // 不管是成功失败,这里都会执行
            });
    });
</script>

5. 遗留问题

  1. OSS返回给客户端的XML没法正常解析

  2. 返回的XML是报错内容,但是不影响文件的正常上传(文件上传返回的是默认的204)。报错内容是(大意)“[AccessDenied]:The bucket you visit is not belong to you.”,查了下文档说原因是“子用户没有Bucket管理的权限(如getBucketAcl CreateBucket、deleteBucket setBucketReferer、 getBucketReferer等)”,调了半天的 RAM(访问控制)也没弄好,不知道是什么原因~

6. 2016/8/31 补遗:

上次留下几个问题,已经解决了,所以过来填坑。

其实这两个问题算是一个问题,在 PostObject 文档里,表单域里有个参数“success_action_status”,描述是“未指定success_action_redirect表单域时,该表单域指定了上传成功后返回给客户端的状态码。 接受值为200, 201, 204(默认)。 如果该域的值为200或者204,OSS返回一个空文档和相应的状态码。 如果该域的值设置为201,OSS返回一个XML文件和201状态码。 如果其值未设置或者设置成一个非法值,OSS返回一个空文档和204状态码。”所以,之前返回不正常的这个问题,只要强行指定返回201状态码,就可以正常收到返回的XML了(并且也没有先前报错的问题了)。

上代码:

<script src="http://cdn.bootcss.com/jquery/2.2.4/jquery.min.js"></script>
<script src="localRZ/lrz.bundle.js"></script>
<script>
    $("#img_input").on("change", function(e) {
        var file = e.target.files[0]; //获取图片资源
        var filename = file.name;

        // 只选择图片文件
        if (!file.type.match('image.*')) {
            return false;
        }
        // LocalResizeIMG写法:
        lrz(file, {width: 200, fieldName: 'osstest'})
            .then(function (rst) {
                var ossData = new FormData();
                // 先请求授权,然后回调
                $.getJSON('ossget.php', function (json) {
                    // 添加配置参数
                    ossData.append('OSSAccessKeyId', json.accessid);
                    ossData.append('policy', json.policy);
                    ossData.append('Signature', json.signature);
                    ossData.append('key', json.dir);
                    ossData.append('success_action_status', 201); // 指定返回的状态码
                    ossData.append('file', rst.file, filename);

                    $.ajax({
                        url: json.host,
                        data: ossData,
                        dataType: 'xml', // 这里加个对返回内容的类型指定
                        processData: false,
                        contentType: false,
                        type: 'POST'
                    }).done(function(data){
                        // 返回的上传信息
                        if ($(data).find('PostResponse')) {
                            var res = $(data).find('PostResponse');
                            console.info('Bucket:' + res.find('Bucket').text() );
                            console.info('Location:' + res.find('Location').text() );
                            console.info('Key:' + res.find('Key').text() );
                            console.info('ETag:' + res.find('ETag').text() );
                        }
                        // 图片预览
                        var img = new Image();
                        img.src = rst.base64;

                        img.onload = function () {
                            $(".preview_box").empty().append(img);
                        };
                    });
                });
                return rst;
            })
            .catch(function (err) {
                // 万一出错了,这里可以捕捉到错误信息
                // 而且以上的then都不会执行
                alert('ERROR:'+err);
            })
            .always(function () {
                // 不管是成功失败,这里都会执行
            });

    });
</script>

最后总结了下,HTTP 一定要学好啊!!(于是哭着滚去看书了……)


【参考资料】

  1. jQuery手册 - AJAX函数

  2. 理解DOMString、Document、FormData、Blob、File、ArrayBuffer数据类型

  3. 对象存储OSS - Web端直传实践:采用服务端签名后直传

  4. 对象存储OSS - API手册 - Post Object

  5. 对象存储OSS - API手册 - PostObject错误及排查

  6. 对象存储OSS - OSS控制台客户端Windows版


某熊猫桑
16.1k 声望19.7k 粉丝

承认自己做不到,坦诚自己不够好。