前端圖片直傳OSS試驗

前段時間參與了一個H5項目,裏邊有個需求是用戶上傳圖片。當時的方案是前端先調用微信的JSSDK選擇圖片並上傳,而後再從後端下載到服務器上。然而用的時候發現客戶端給的圖片有大有小,可是因爲用了微信的接口,圖片在下載以前是無法控制的。後來在想能不能調用HTML5原生的文件上傳接口,另外還能夠配合阿里雲的OSS對圖片作進一步處理,因此就有了這篇文章。php

1. HTML5原生上傳

其實以前也有想過用原生的,可手裏的項目全是微信平臺的H5,原生上傳一直被告知有兼容性問題,因此這個方案一直是被擱置的;只是此次以爲用微信接口實在不爽才從新翻出來的,沒想到意外發現手裏的米4竟然能夠正經常使用。。好了閒話不說,上代碼:css

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

HTML部分主要就是那個input,至於下邊那個div,主要是留着放圖片預覽用的。html

<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");
}

而後處理下權限啥的,就能跑啦。jquery

2. 前端壓縮(localResizeIMG)

localResizeIMG 是個好插件,用法也很簡單,把 GitHub 裏的 dist 文件夾拖下來改個名(我改了個「localRZ」),而後直接引用 lrz.bundle.js 文件就好了:git

<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 的 文檔 寫的挺清楚的,哪裏不明白的話能夠過去看看。github

3. 美化上傳按鈕

原生的文件上傳控件略醜,因此通常是要美化一下。
HTML:ajax

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

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

放一個 lable 上去,而後隱藏掉原有的 input:json

<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版

相關文章
相關標籤/搜索