部分安卓手機微信瀏覽器中使用XMLHttpRequest 2上傳圖片顯示位元組數為0的解決辦法

Jaxu發表於2016-05-17

  前端JS中使用XMLHttpRequest 2上傳圖片到伺服器,PC端和大部分手機上都正常,但在少部分安卓手機上上傳失敗,伺服器上檢視圖片,顯示位元組數為0。下面是上傳圖片的核心程式碼:

 HTML

<input type="file" id="choose" capture="camera" accept="image/*">

JavaScript

var filechooser = document.getElementById("choose");

filechooser.onchange = function () {
    var _this = $(this);
    if (!this.files.length) return;
    var files = Array.prototype.slice.call(this.files);
    if (files.length > 1) {
        alert("一次只能上傳1張圖片");
        return;
    }
    files.forEach(function (file, i) {
        if (!/\/(?:jpeg|png|gif)/i.test(file.type)) return;
        var reader = new FileReader();
        reader.onload = function () {
            var result = this.result;
            upload(result, file.type);
        };
        reader.readAsDataURL(file);
    });
};

function upload(basestr, type){
    var xhr = new XMLHttpRequest();
    var text = window.atob(basestr.split(",")[1]);
    var buffer = new Uint8Array(text.length);
    var pecent = 0;
    for (var i = 0; i < text.length; i++) {
        buffer[i] = text.charCodeAt(i);
    }
    var blob = getBlob(buffer, type);
    var formdata = new FormData();
    formdata.append('imagefile', blob);

    xhr.open('post', '/uploadtest');
    xhr.onreadystatechange = function () {
        if (xhr.readyState == 4 && xhr.status == 200) {
            var jsonData = JSON.parse(xhr.responseText);
            console.log(jsonData);
        }
    };

    //利用progress事件顯示資料傳送進度
    xhr.upload.addEventListener('progress', function (e) {
        pecent = ~~(100 * e.loaded / e.total) / 2;
        // 利用pecent來顯示上傳進度
    }, false);

    xhr.send(formdata);
}

function getBlob(buffer, format){
    var Builder = window.WebKitBlobBuilder || window.MozBlobBuilder;
    if(Builder){
        var builder = new Builder();
        builder.append(buffer);
        return builder.getBlob(format);
    } else {
        return new window.Blob([ buffer ], {type: format});
    }
}

  上述程式碼使用FormData來實現表單資料提交。FormData是一種針對XHR2設計的新型資料型別,使用它我們可以很方便地實時以JavaScript建立HTML <Form>,然後通過AJAX提交該表單。在上述程式碼中,提交的表單中的欄位名為imagefile,值是blob,這是一個通過getBlob函式構造並返回的檔案Blob。通過該方法上傳檔案簡單直觀。

  然後我們在服務端接收並儲存圖片,並返回已上傳的圖片的資訊。下面是Node.js程式碼的示例:

var Q = require('q');
var fs = require('fs');
var path = require('path');
var formidable = require('formidable');
var moment = require('moment');var imageUpload = function (){ };

imageUpload.prototype.useFormParseCallback = function(req){
    var deferred = Q.defer();

    var form = new formidable.IncomingForm();
    form.parse(req, deferred.makeNodeResolver());
    return deferred.promise;
};

imageUpload.prototype.uploadImageTest = function(req){
    var pathName = 'uploadImgs/dealInfo/';
    var uploadPath = path.join(__dirname, '../../public/', pathName);

    return this.useFormParseCallback(req).then(function(files){
        var file = files[1].imagefile;
        var fileType = files[1].imagefile.type.split('/')[1];
        var newFileName = 'upload_' + moment().format('x') + Math.random().toString().substr(2, 10) + '.' + fileType;

        var readStream = fs.createReadStream(file.path);
        var writeStream = fs.createWriteStream(uploadPath + newFileName);
         
        var deferred = Q.defer();
        readStream.pipe(writeStream);
        readStream.on('end', deferred.makeNodeResolver());
        return deferred.promise.then(function() {
            fs.unlinkSync(file.path);
            return {
                fileName: newFileName,
                filePath: '/' + pathName + newFileName,
                fileSize: file.size/1024 > 1024 ? (~~(10*file.size/1024/1024))/10 + "MB" : ~~(file.size/1024) + "KB"
            };
        });
    });
};

module.exports = imageUpload;

  我們使用formidable這個包來接收上傳檔案的資料,然後將檔案儲存到/public/uploadImgs/dealInfo目錄下(假定已在express中將public設定為static的根目錄),並將圖片按照指定的規則重新命名,以保證上傳圖片不會因為名稱相同而被覆蓋。另外,程式碼中使用Q來避免直接使用回撥函式,以更好地對函式功能進行分離。

  上面的程式碼在PC端瀏覽器以及大部分主流移動裝置上都能正常工作,但是少部分Android裝置上卻會出現上傳的圖片位元組數為0的情況。具體的原因大家可以看下面幾個網頁中的描述:

http://www.oschina.net/question/2502182_2139420?fromerr=qwYwJQK8

https://github.com/fex-team/webuploader/issues/185

https://code.google.com/p/android/issues/detail?id=39882

  就是說這個是Android的一個bug!

  那如何解決呢?

  其實從上面給出的頁面中可以找到答案,就是我們得換一種檔案上傳方式。在XHR2中,除了以Blob的方式上傳檔案外,還可以ArrayBuffer的方式上傳檔案。下面是修改之後的前端JavaScript程式碼:

var filechooser = document.getElementById("choose");

filechooser.onchange = function () {
    var _this = $(this);
    if (!this.files.length) return;
    var files = Array.prototype.slice.call(this.files);
    if (files.length > 1) {
        alert("一次只能上傳1張圖片");
        return;
    }
    files.forEach(function (file, i) {
        if (!/\/(?:jpeg|png|gif)/i.test(file.type)) return;
        var reader = new FileReader();
        reader.onload = function () {
            var result = this.result;
            upload(result, file.type);
        };
        reader.readAsDataURL(file);
    });
};

function upload(basestr, type){
    var xhr = new XMLHttpRequest();
    var text = window.atob(basestr.split(",")[1]);
    var buffer = new Uint8Array(text.length);
    var pecent = 0;
    for (var i = 0; i < text.length; i++) {
        buffer[i] = text.charCodeAt(i);
    }

    xhr.open('post', '/uploadtest?filetype=' + type.split('/')[1]);
    xhr.setRequestHeader('Content-Type', 'application/octet-stream');
    xhr.onreadystatechange = function () {
        if (xhr.readyState == 4 && xhr.status == 200) {
            var jsonData = JSON.parse(xhr.responseText);
            console.log(jsonData);
        }
    };
    //利用progress事件顯示資料傳送進度
    xhr.upload.addEventListener('progress', function (e) {
        pecent = ~~(100 * e.loaded / e.total) / 2;
        // 利用pecent來顯示上傳進度
    }, false);

    xhr.send(buffer.buffer); // 以ArrayBuffer的方式上傳圖片
}

  我將有變化的地方加了高亮顯示。以ArrayBuffer方式上傳圖片必須新增'application/octet-stream'的RequestHeader,否則伺服器無法響應請求。另外,通過這種方式上傳圖片我們也無法從表單資料中獲取到檔案型別,可以將檔案型別以query的方式傳到伺服器,然後伺服器根據檔案型別來生成對應的檔案,以下是經過少量修改之後的伺服器程式碼:

imageUpload.prototype.uploadImageTest = function(req){
    var pathName = 'uploadImgs/dealInfo/';
    var uploadPath = path.join(__dirname, '../../public/', pathName);

    return this.useFormParseCallback(req).then(function(files){
        var file = files[1].file;
        var fileType = req.query.filetype ? ('.' + req.query.filetype) : '.png';
        var newFileName = 'upload_' + moment().format('x') + Math.random().toString().substr(2, 10) + '.' + fileType;

        var readStream = fs.createReadStream(file.path);
        var writeStream = fs.createWriteStream(uploadPath + newFileName);
         
        var deferred = Q.defer();
        readStream.pipe(writeStream);
        readStream.on('end', deferred.makeNodeResolver());
        return deferred.promise.then(function() {
            fs.unlinkSync(file.path);
            return {
                fileName: newFileName,
                filePath: '/' + pathName + newFileName,
                fileSize: file.size/1024 > 1024 ? (~~(10*file.size/1024/1024))/10 + "MB" : ~~(file.size/1024) + "KB"
            };
        });
    });
};

  修改之後的程式碼可以支援Android手機,包括微信瀏覽器。注意不是所有的Android手機都會存在該問題,如果你發現在Andriod手機上無法上傳圖片,尤其是在微信瀏覽器中,則可以嘗試下上面的方法。

 

參考頁面:

http://www.html5rocks.com/zh/tutorials/file/xhr2/

http://www.zhangxinxu.com/wordpress/2013/10/understand-domstring-document-formdata-blob-file-arraybuffer/

相關文章