JS 實現上傳圖片


簡介

上傳是個老生常談的話題了,多數情況下各位想必用的是uplodify,webUploader之類的插件,但近期團隊定制組件的時候,筆者總覺得插件太重,許多功能用不到,那么就自己練手寫了一個demo,並且支持圖片拖拽排序。支持chrome 31及以上,IE就呵呵了。不過筆者的團隊就是不用兼容IE,所以任性。。另外,后端處理部分本篇不會詳細討論,請直接查看下面的源碼。

單圖上傳

上傳主要涉及 XMLHttpRequest Level 2的API:FormData。下面的腳本chrome 31版后才會兼容。

css部分使用了一個障眼法,將input type=file的表單項設置為opacity:0的,然后絕對定位撐滿父容器。這樣一來用戶看到的只有父容器的樣子,而點擊到的元素input type=file卻是透明的。

.photo-item, .photo-add {
    position: relative;
        float: left;
        width: 120px;
        height: 90px;
        margin-bottom: 52px;
        margin-right: 16px;
    }
}

.item-image {
    display: block;
    width: 100%;
    height: 100%;
}

.uploader-file {
    opacity: 0;
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    cursor: pointer;
}

accept屬性表示可以選擇的文件MIME類型,多個MIME類型用英文逗號分開,常見MIME類型見這里。但是accept會使得瀏覽器調用文件選擇界面的速度變慢,大概是與瀏覽器需要篩選不同類型的文件有關,不使用accept屬性的話就不會有嚴重的延遲。

<div class="photo-add">
    <img class="item-image" src="http://7xn4mw.com1.z0.glb.clouddn.com/16-9-13/13827291.jpg" alt="">

    <input type="file" accept="image/*"
        name="uploader-input" 
        class="uploader-file"
        id="upload">
</div>

<div id="box"></div>

js需要監聽input的onchange事件,從而拿到file對象,塞進FormData的實例對象里,就能用ajax提交。

document.getElementById('upload').addEventListener('change', function (event) {
    var $file = event.currentTarget;
    var formData = new FormData();
    var file = $file.files;
    formData = new FormData();
    formData.append(file[0].name, file[0]);
    $.ajax({
        url: '/upload',
        type: 'POST',
        dataType: 'json',
        data: formData,
        contentType: false,
        processData: false
    })
    .done(data => {
        $('#box').append(`<div class="photo-item">
            <img class="item-image" width="100%" height="100%" src="${data.url}"/>
        </div>`);
    })
    .fail(data => {
        console.log(data);
    });
});

多選上傳

注意多了一個multiple屬性,是否可以選擇多個文件,多個文件時其value值為第一個文件的虛擬路徑。

<input type="file" accept="image/*" multiple
    name="uploader-input" 
    class="uploader-file"
    id="upload">

注意需要對file對象進行遍歷,由於服務器暫時做的很簡單,只能響應單圖的上傳請求,所以需要多次發起ajax。

document.getElementById('upload').addEventListener('change', function (event) {
    var $file = event.currentTarget;
    var formData = new FormData();
    var file = $file.files;
    for (var i = 0; i < file.length; i++) {
        // 文件名稱,文件對象
        formData = new FormData();
        formData.append(file[i].name, file[i]);
        $.ajax({
            url: '/upload',
            type: 'POST',
            dataType: 'json',
            data: formData,
            contentType: false,
            processData: false
        })
        .done(data => {
            $('#box').append(`<div class="photo-item">
                <img class="item-image" width="100%" height="100%" src="${data.url}"/>
            </div>`);
        })
        .fail(data => {
            console.log(data);
        });
    }
});

drag and drop api

這里已經支持外部文件系統拖拽圖片到div.photo-add上可上傳,瀏覽器已經替我們做了處理了,為了練習drag api,於是嘗試加入上傳的圖可以拖動排序的需求,則可以使用drag and drop的api。chrome 7+以上支持。這里只使用了三個事件:

  • ondragstart 當拖拽元素開始被拖拽的時候觸發的事件,此事件作用在被拖曳元素上
  • ondragover 拖拽元素在目標元素上移動的時候觸發的事件,此事件作用在目標元素上
  • ondrop 被拖拽的元素在目標元素上同時鼠標放開觸發的事件,此事件作用在目標元素上
......
var temp;
$('#box')
.on('dragstart', '.photo-item', function (e) {
    temp = this;
})
.on('dragover', '.photo-item', function (e) {
    //此事件切記要preventDefault,否則接下來將不會觸發drop事件
    e.preventDefault();
})
.on('drop', '.photo-item', function (e) {
    var sourceHTML = temp.innerHTML;
    temp.innerHTML = this.innerHTML;
    this.innerHTML = sourceHTML;
});

壓縮圖片

需要canvas的支持,原理是利用了canvas定好所要生成的寬高,並且HTMLCanvasElement.toDataURL()的第二個參數代表着清晰度。這樣就完成了裁剪和壓縮的步驟。
window.atob()表示從base64字符中解碼,window.btob()表示編碼為base64字符,chrome 4+支持。
ArrayBuffer表示二進制數據的原始緩沖區,該緩沖區用於存儲各種類型化數組的數據。 無法直接讀取或寫入ArrayBuffer,但可根據需要將其傳遞到類型化數組或 DataView 對象 來解釋原始緩沖區。
blob的api是為了讓buffer轉化為二進制文件,在chrome 20版以上就支持

js需要改寫成下面的樣子:

var uploadFn = function (formData) {
    //發送到服務端
    $.ajax({
        url: '/upload2',
        type: 'POST',
        dataType: 'json',
        data: formData,
        contentType: false,
        processData: false
    })
    .done(res => {
        $('#box').append(`<div class="photo-item">
            <img class="item-image" width="100%" height="100%" src="${res.url}"/>
        </div>`);
        console.log(res.path);
    })
    .fail(res => {
        console.log(res);
    });
};
var compass = function (imgObj, type, maxWidth, maxHeight, encoderOptions) {

    //生成比例
    if (imgObj.height > maxHeight) { //按最大高度等比縮放
        imgObj.width = Math.round(imgObj.width * (maxHeight / imgObj.height));
        imgObj.height = maxHeight;
    }
    if (imgObj.width > maxWidth) { //按最大高度等比縮放
        imgObj.height = Math.round(imgObj.height * (maxWidth / imgObj.width));
        imgObj.width = maxWidth;
    }

    //生成canvas
    var $canvas = document.createElement('canvas');
    var ctx = $canvas.getContext('2d');
    $canvas.width = imgObj.width;
    $canvas.height = imgObj.height;
    ctx.drawImage(imgObj, 0, 0, $canvas.width, $canvas.height);
    //canvas.toDataURL的第二個參數決定了圖片的質量
    var base64 = $canvas.toDataURL(type, encoderOptions);
    $canvas = null;

    //window.atob()把數據從base64格式中解碼,接着壓入二進制數據的原始緩沖區,最后使用blob轉為二進制文件。
    var text = window.atob(base64.split(',')[1]);
    var buffer = new ArrayBuffer(text.length);
    var ubuffer = new Uint8Array(buffer);
    for (var i = 0; i < text.length; i++) {
        ubuffer[i] = text.charCodeAt(i);
    }
    var Builder = window.WebKitBlobBuilder || window.MozBlobBuilder;
    var blob;
    if (Builder) {
        var builder = new Builder();
        builder.append(buffer);
        blob = builder.getBlob(type);
    } else {
        blob = new window.Blob([buffer], {type: type});
    }

    return blob;
};

document
.getElementById('upload')
.addEventListener('change', function (event) {

    var $file = event.currentTarget;
    var file = $file.files;
    for (var i = 0; i < file.length; i++) {
        var url = window.URL.createObjectURL(file[i]);
        var $img = new Image();
        $img.src = url;
        $img.onload = (function (sourceFile) {
            return function () {
                var formData = new FormData();

                //png圖片不需要canvas壓縮,不然會越壓越大
                if (sourceFile.type === 'image/png') {
                    formData.append('upload', sourceFile, sourceFile.name);
                } else {
                    formData.append('upload',
                        compass(this, sourceFile.type, 1000, 800, 0.65),
                        sourceFile.name);
                }
                uploadFn(formData);
                
            }
        })(file[i])
    }
});

本地預覽並壓縮

本地預覽需要依賴fileReader API。

function compressImg(imgData, file, maxHeight, maxWidth, onCompress) {
    if (!imgData) return;
    onCompress = onCompress || function() {};
    maxHeight = maxHeight || 1000; //默認最大高度200px
    maxWidth = maxWidth || 1000; //默認最大高度200px
    
    var canvas = document.createElement('canvas');
    var img = new Image();
    img.onload = function() {
        if (img.height > maxHeight) { //按最大高度等比縮放
            img.width = Math.round(img.width * (maxHeight / img.height));
            img.height = maxHeight;
        }
        if (img.width > maxWidth) { //按最大高度等比縮放
            img.height = Math.round(img.height * (maxWidth / img.width));
            img.width = maxWidth;
        }
        var ctx = canvas.getContext('2d');
        canvas.width = img.width;
        canvas.height = img.height;

        ctx.clearRect(0, 0, canvas.width, canvas.height); // canvas清屏
        //重置canvans寬高 canvas.width = img.width; canvas.height = img.height;
        ctx.drawImage(img, 0, 0, img.width, img.height); // 將圖像繪制到canvas上 

        var base64 = canvas.toDataURL(file.type, 0.65);
        var text = window.atob(base64.split(',')[1]);
        var buffer = new ArrayBuffer(text.length);
        var ubuffer = new Uint8Array(buffer);
        for (var i = 0; i < text.length; i++) {
            ubuffer[i] = text.charCodeAt(i);
        }
        var Builder = window.WebKitBlobBuilder || window.MozBlobBuilder;
        var blob;
        if (Builder) {
            var builder = new Builder();
            builder.append(buffer);
            blob = builder.getBlob(file.type);
        } else {
            blob = new window.Blob([buffer], {type: file.type});
        }
        //必須等壓縮完才讀取canvas值,否則canvas內容是黑帆布
        //canvas.toDataURL的第二個參數決定了圖片的質量,筆者在此寫死0.65
        onCompress(blob, file.name, file.type); 
    };

    // 記住必須先綁定事件,才能設置src屬性,否則img沒內容可以畫到canvas
    img.src = imgData;
}

document
.getElementById('upload')
.addEventListener('change', function (event) {

    var $file = event.currentTarget;
    var file = $file.files;
    var FR;
    for (var i = 0; i < file.length; i++) {
        FR = new FileReader();
        FR.readAsDataURL(file[i]); //先注冊onload,再讀取文件內容,否則讀取內容是空的
        FR.onload = (function (targetFile) {
            return function (previewObj) {
                compressImg(previewObj.target.result, targetFile, 800, 1000,
                    function(compressData, name, type) {

                    var formData = new FormData();
                    //壓縮完成后執行的callback
                    formData.append('upload', compressData, name);
                    $.ajax({
                        url: '/upload2',
                        type: 'POST',
                        dataType: 'json',
                        data: formData,
                        contentType: false,
                        processData: false
                    })
                    .done(res => {
                        console.log(res.path);
                    })
                    .fail(res => {
                        console.log(res);
                    });
                    $('#box').append(`<div class="photo-item">
                        <img class="item-image" width="100%" height="100%" src="${previewObj.target.result}"/>
                    </div>`);
                });
            }
        })(file[i]);
    }
});

思考

上面的代碼只是演示的demo,當然有很多改進的空間,比如說:

  • 上傳圖片的刪除,酷一點的當然可以將圖片拖到頁面某個區域直接就刪除。
  • 上傳顯示進度百分比,這個效果需要ajax請求里加入xhr參數

服務端

服務端是一個結構分層的node處理圖片上傳,抄自node入門篇里的結構:

  • /staticfile 靜態文件所在,主要是jq和uploader插件。
  • /tmp 圖片存儲文件夾。
  • index.js 請求控制,控制某請求調用某方法進行的。
  • requestHandlers.js 請求處理,邏輯最重的地方。
  • router.js 請求的路由,控制請求接受來后調用哪個請求控制組的。
  • server.js 應用啟動入口。

這里略過描述,有需要的直接去下文檔下面找到github源碼地址看。

源碼

源代碼包括node版服務端,路徑:github地址,目錄如下:

  • /staticfile 靜態文件所在,主要是jq和uploadify插件。
  • /tmp 上傳圖片后存儲文件夾的位子。
  • index.js 服務端:請求控制,控制某請求調用某方法進行的。
  • requestHandlers.js 服務端:請求處理,邏輯最重的地方。
  • router.js 服務端:請求的路由,控制請求接受來后調用哪個請求控制組的。
  • server.js 服務端:應用啟動入口。
  • uploader.html 前端頁面,演示了用uploadify插件上傳,訪問localhost:8066/uploader.html可以看到。
  • uploader1.html 前端頁面,演示了h5多圖上傳,訪問localhost:8066/uploader1.html可以看到。
  • uploader2.html 前端頁面,演示了圖片上傳前壓縮,訪問localhost:8066/uploader2.html可以看到。
  • uploader3.html 前端頁面,演示了圖片壓縮並預覽上傳,訪問localhost:8066/uploader3.html可以看到。

找到代碼目錄后運行命令

npm install formidable
node index.js

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM