基於 jq 實現拖拽上傳 APK 文件,js解析 APK 信息

技術棧

參考:前端解析ipa、apk安裝包信息 —— app-info-parserjavascript

支持功能

  • 點擊或拖拽上傳 apk 文件
  • 校驗文件類型及文件大小
  • js 解析 apk 文件信息展現並經過上傳接口提交給後端
  • 支持上傳過程當中取消上傳
  • 支持上傳成功顯示上傳信息
  • 支持解析、上傳等友好提示
  • 支持從歷史記錄(全部已上傳文件)中選擇一個
  • 支持假文件處理,好比 .txt 文件改成 .apk 文件
  • 上傳進度實時更新,百分比,B/s
  • 拖拽進入拖拽區時,高亮顯示

demo 預覽

說明php

因爲上傳接口須要後端接口的支持,因此無法用靜態頁面展現完整的交互。所以,在這兒放個預覽圖。css

demo

爲了不 gif 圖太大,只錄屏了點擊上傳成功的狀況。其餘狀況沒錄屏,可自行下載 demo ,搭建後端環境,模擬上傳接口實現。demo 中用 php 語言模擬實現了上傳接口。源碼地址html

難點

  • js 解析 APK 文件信息
  • 拖拽上傳,點擊上傳和拖拽上傳綁定到一塊兒
  • 在上傳以前不知道 APK 文件信息,須要執行上傳操做過程當中將解析的文件信息做爲參數放到上傳接口中
  • 上傳過程當中取消上傳
  • 假文件解析錯誤處理,js 監控控制檯錯誤

實現

1. js 解析 APK 文件信息前端

通過查閱,瞭解到 APK 文件的本質就是一個壓縮包,其中包含一堆XML文件,資產和類文件。javascript 解析 APK 文件信息,要作的就是先解壓,而後讀取其中相關的文件,就能獲得文件信息了。java

難點在解壓上,參考的基本都須要藉助 node 環境。因爲如今維護的系統是基於 jquery 環境的。因此最終採用了
前端解析ipa、apk安裝包信息 —— app-info-parser 該文的方案,很好的解決了問題。在此很是感謝該做者。node

// apk 文件解析
var parser = new AppInfoParser(data.files[0]);
parser.parse().then(function(result) {
    uploadMod.doms.uploadErr.html('');
    var appInfo = result.application || {};
    var formAppInfo = {
        name: appInfo.label ? (Array.isArray(appInfo.label) ? appInfo.label[0] : appInfo.label) : '',
        package: result.package,
        version: result.versionName,
        version_code: result.versionCode
    };
    
    // 省略其餘操做代碼...
}).catch(function (err) {
    uploadMod.doms.uploadErr.html('文件解析錯誤,請從新上傳');
});

說明:jquery

  • 因爲 app-info-parser 底層用了 async 語法,在 IE 下是不兼容的。在 firefox、chrome 下是正常的。
  • 上傳假 APK 文件,不能處理,js 腳本會報錯:File format is not recognized.。目前想到的解決方案是 js 監聽錯誤,而後進行處理。如有更好想法的,歡迎@我。在此提早感謝。
// console.error() 監控處理
consoleError = window.console.error;
window.console.error = function () {
    consoleError && consoleError.apply(window, arguments);
    for (var info in arguments) {
        if (arguments[info] == 'File format is not recognized.') {
            $('#app_parse').html('<p style="color:red;">因爲您上傳了非真正的 APK 文件,致使腳本解析出錯,即將從新刷新頁面,給您帶來很差的體驗,敬請原諒</p>');
            setTimeout(function () {
                history.go(0);
            }, 3000);
            return false;
        }
    }
};

爲了不頁面其它錯誤,致使腳本沒法運行,所以作了頁面刷新。android

2. 拖拽上傳,點擊上傳和拖拽上傳綁定到一塊兒git

在作這個功能前,想到拖拽上傳能夠利用 H5 的拖拽功能及原生 js 的 file 文件上傳實現,但須要處理兼容性問題。後來想到系統中已經引入了 jquery.fileupload 庫,因而特意翻閱了文檔,支持拖拽上傳。所以採用該庫實現拖拽上傳功能。

html 佈局以下:

<div class="upload-area" id="upload_area">
    <i class="icon-upload"></i>
    <p class="upload-text">將安裝包拖拽至此上傳或 <em>選擇文件</em></p>
    <p class="upload-tip">支持 APK 文件,最大不超過 300 MB</p>
    <input type="file" id="upload_input" name="file" accept="application/vnd.android.package-archive" data-size="300"/>
</div>

如何將 拖拽、點擊 一塊兒處理,用一個上傳方法實現,而不是分開須要實現2遍?

想法是,點擊外層容器,觸發 input 點擊事件。前提是須要實現 input 點擊事件,而且阻止冒泡事件,由於外層也有點擊事件。

$('body').on('click', '#upload_input', function (e) {
    e.stopPropagation();
    uploadMod.methods.fileUpload();
}).on('click drop dragenter dragover dragleave', '#upload_area', function(e) {
    e.preventDefault();
    uploadMod.doms.uploadErr.html('');
    
    switch (e.type) {
        case 'click':
            $('#upload_input').val(null);
            $('#upload_input').click();
            break;
        case 'drop':
            uploadMod.doms.uploadArea.removeClass('active');
            $('#upload_input').val(null);
            uploadMod.methods.fileUpload();
            break;
        case 'dragenter':
        case 'dragover':
            uploadMod.doms.uploadArea.addClass('active');
            break;
        case 'dragleave':
            uploadMod.doms.uploadArea.removeClass('active');
            break;
    }
})

實現了拖拽進入高亮、遠離恢復。須要注意的是,$('#upload_input') 不能用緩存的變量。不然會致使二次點擊上傳失效,沒法觸發點擊打開文件窗口。以及此時拖拽上傳一個正確的文件,會觸發 2 次文件上傳。發送 2 次上傳接口。感興趣的朋友能夠本身用緩存的試一下。

案例復現:

  • 點擊假的內容爲空的 apk 文件,會提示:文件尺寸不對。
  • 此時,第二次點擊,沒法觸發 input 的點擊事件。反覆屢次依然無效。
  • 此時,經過拖拽上傳,可以正常執行,可是會觸發 2 次上傳處理,解析 2 次文件,發送 2 次上傳接口請求。

3. 在上傳以前不知道 APK 文件信息,須要執行上傳操做過程當中將解析的文件信息做爲參數放到上傳接口中

以前作過的上傳,是在上傳前就已經知道在上傳時須要提交的額外參數值。

$('#upload_input').fileupload({
    url: 'http://localhost:80/jq-drag-upload-apk-parse/upload.php',
    dataType: 'json',
    formData: params, // params 爲 js 對象,是須要提交的參數
    multi: false,
    // 省略....
})

但如今,在上傳前是不知道參數值的,須要在執行上傳操做,拿到上傳文件信息,並解析出上傳文件的信息,而後將解析信息作爲參數值放到上傳請求中。那怎麼作呢,研究了好久,才找到。

$('#upload_input').fileupload({
    url: 'http://localhost:80/jq-drag-upload-apk-parse/upload.php',
    dataType: 'json',
    formData: params, // params 爲 js 對象,是須要提交的參數
    multi: false,
    add: function (e, data) {
        // 省略文件類型及大小校驗
        // 省略 APK 文件解析及進度條等的 UI 初始化
        
        $(e.target).fileupload(
            'option',
            'formData',
            formAppInfo // APK 解析出的數據
        );
        data.submit();
    },
    // 省略....
})

4. 上傳過程當中取消上傳

這個相對比較容易。利用上傳回調中的 data.abort() 便可實現。須要處理的是,在 add() 方法裏須要先在外層緩存一下 data,才方便對其的調用。

$('#upload_input').fileupload({
    url: 'http://localhost:80/jq-drag-upload-apk-parse/upload.php',
    dataType: 'json',
    formData: params, // params 爲 js 對象,是須要提交的參數
    multi: false,
    add: function (e, data) {
        // 省略文件類型及大小校驗
        // 省略 APK 文件解析及進度條等的 UI 初始化
        
        // 外層緩存,方便調取消上傳
        uploadMod.uploadXHR = data;
        
        $(e.target).fileupload(
            'option',
            'formData',
            formAppInfo // APK 解析出的數據
        );
        data.submit();
    },
    fail: function(e, data) {
        if (data.errorThrown == 'abort') {
            uploadMod.doms.uploadErr.html('已取消上傳,可從新上傳');
        } else {
            uploadMod.doms.uploadErr.html('上傳失敗,請從新上傳');
        }
    },
    // 省略....
})
$('body').on('click', '#upload_cancel', function () {
    uploadMod.uploadXHR.abort();
})

5. 文件上傳的主要代碼

fileCheck: function(e, data) {
    // 文件格式及文件大小校驗
    var acceptFileTypes = uploadMod.doms.uploadInput.attr('accept');
    var supportFileTypes = ['apk']; // 經過name後綴再校驗一次,避免獲取不到type的狀況
    var maxSize = uploadMod.doms.uploadInput.data('size') * 1024 * 1024; // 單位mb,須要轉換爲b
    var fileTypeFlag = data.originalFiles.every(function(item) {
        if (item.type) {
           return acceptFileTypes.indexOf(item.type) > -1;
        } else {
           var splits = (item.name || file).split('.');
           var fileType = splits[splits.length - 1];
           return supportFileTypes.indexOf(fileType) > -1;
        }
    });
    if (!fileTypeFlag) {
        uploadMod.doms.uploadErr.html('請上傳 APK 文件');
        return false;
    }
    var fileSizeFlag = data.originalFiles.every(function(item) {
        return item.size > 0 && item.size <= maxSize;
    });
    if (!fileSizeFlag) {
        data = {};
        uploadMod.doms.uploadErr.html('文件大小不正確');
        return false;
    }
    
    uploadMod.doms.progressWrap.show();
    var $appParse = uploadMod.doms.progressWrap.find('.app-parse'),
        $progressCon = uploadMod.doms.progressWrap.find('.con');
    $appParse.show();
    $progressCon.hide();

    // apk 文件解析
    var parser = new AppInfoParser(data.files[0]);
    parser.parse().then(function(result) {
        uploadMod.doms.uploadErr.html('');
        var appInfo = result.application || {};
        var formAppInfo = {
            name: appInfo.label ? (Array.isArray(appInfo.label) ? appInfo.label[0] : appInfo.label) : '',
            package: result.package,
            version: result.versionName,
            version_code: result.versionCode
        };

        // 進度條初始化
        $appParse.hide();
        $progressCon.show();
        if (result.icon) {
            uploadMod.doms.progressWrap.find('.icon-app').css('background-image', 'url("' + result.icon + '")');
        }
        uploadMod.doms.progressWrap.find('.name').html(formAppInfo.name);
        uploadMod.doms.progressWrap.find('.package').html(formAppInfo.package);
        uploadMod.doms.progressWrap.find('.version').html(formAppInfo.version);
        uploadMod.doms.progressWrap.find('.version-code').html(formAppInfo.version_code);
        uploadMod.doms.progressWrap.find('.progress').css('width', 0);
        uploadMod.doms.progressWrap.find('.num').html(0);
        uploadMod.doms.progressWrap.find('.size').html(0);

        // 設置上傳接口參數
        uploadMod.uploadXHR = data;
        $(e.target).fileupload(
            'option',
            'formData',
            formAppInfo
        );
        data.submit();
    }).catch(function (err) {
        uploadMod.doms.progressWrap.hide();
        uploadMod.doms.uploadErr.html('文件解析錯誤,請從新上傳');
        data.abort();
    });
    
    // console.error() 監控處理
    consoleError = window.console.error;
    window.console.error = function () {
        consoleError && consoleError.apply(window, arguments);
        for (var info in arguments) {
            if (arguments[info] == 'File format is not recognized.') {
                $('#app_parse').html('<p style="color:red;">因爲您上傳了非真正的 APK 文件,致使腳本解析出錯,即將從新刷新頁面,給您帶來很差的體驗,敬請原諒</p>');
                setTimeout(function () {
                    history.go(0);
                }, 3000);
                return false;
            }
        }
    };
},
fileUpload: function(el) {
    $('#upload_input').fileupload({
        url: 'http://localhost:80/jq-drag-upload-apk-parse/upload.php',
        dataType: 'json',
        multi: false,
        add: uploadMod.methods.fileCheck,
        paste: function () { return false; },
        done: function(e, data) { // 上傳成功回調
            var result = data.result;
            if (result && result.flag && result.data) {
                uploadMod.doms.uploadErr.html(result.msg || '上傳成功');
                uploadMod.data.selectedAPK = result.data;
                uploadMod.methods.renderHistory(result.data);
            } else {
                uploadMod.doms.progressWrap.hide();
                uploadMod.doms.uploadErr.html(result.msg || '上傳失敗');
            }
        },
        fail: function(e, data) {
            if (data.errorThrown == 'abort') {
                uploadMod.doms.uploadErr.html('已取消上傳,可從新上傳');
            } else {
                uploadMod.doms.uploadErr.html('上傳失敗,請從新上傳');
            }
        },
        progressall: function(e, data) {
            var progress = parseInt(data.loaded / data.total * 100, 10);
            uploadMod.doms.progressWrap.find('.progress').css('width', progress + '%');
            uploadMod.doms.progressWrap.find('.num').html(progress);
            uploadMod.doms.progressWrap.find('.size').html(bytesToSize(data.bitrate));

            function bytesToSize(bit) {
                if (bit === 0) return '0 B';
                var bytes = bit / 8;
                var k = 1024,
                    sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
                    i = Math.floor(Math.log(bytes) / Math.log(k));
             
               return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i];
            }
        }
    })
},

php 環境簡單搭建

  • 下載 xampp 集成環境包進行安裝
  • 在 demo 項目解壓拷貝到安裝目錄下的 htdocs 的目錄下,個人目錄是 C:\xampp\htdocs\jq-drag-upload-apk-parse
  • 因爲 php 上傳有限制,須要改文件C:\xampp\php\php.ini,須要修改的點:
    • max_execution_time = 0,默認 30 秒,0 爲無限制
    • post_max_size = 500M,默認 2M
    • upload_max_filesize = 100M,默認 8M
    • ps:參考PHP上傳大小限制修改
  • 最後點擊安裝目錄下的(C:\xampp)的 xampp.control.exe 打開界面,在打開界面中,將 Apache 對應的 Actions 開啓
  • 在瀏覽器窗口輸入http://localhost/jq-drag-upload-apk-parse/index.html
  • 便可完整查看 demo 效果
  • 源碼地址
相關文章
相關標籤/搜索