技術棧
參考:前端解析ipa、apk安裝包信息 —— app-info-parser
支持功能
- 點擊或拖拽上傳 apk 文件
- 校驗文件類型及文件大小
- js 解析 apk 文件信息展示並通過上傳接口提交給后端
- 支持上傳過程中取消上傳
- 支持上傳成功顯示上傳信息
- 支持解析、上傳等友好提示
- 支持從歷史記錄(所有已上傳文件)中選擇一個
- 支持假文件處理,比如 .txt 文件改為 .apk 文件
- 上傳進度實時更新,百分比,B/s
- 拖拽進入拖拽區時,高亮顯示
demo 預覽
說明
由於上傳接口需要后端接口的支持,所以沒法用靜態頁面展示完整的交互。因此,在這兒放個預覽圖。
為了避免 gif 圖太大,只錄屏了點擊上傳成功的情況。其他情況沒錄屏,可自行下載 demo ,搭建后端環境,模擬上傳接口實現。demo 中用 php 語言模擬實現了上傳接口。源碼地址
難點
- js 解析 APK 文件信息
- 拖拽上傳,點擊上傳和拖拽上傳綁定到一起
- 在上傳之前不知道 APK 文件信息,需要執行上傳操作過程中將解析的文件信息作為參數放到上傳接口中
- 上傳過程中取消上傳
- 假文件解析錯誤處理,js 監控控制台錯誤
實現
1. js 解析 APK 文件信息
經過查閱,了解到 APK 文件的本質就是一個壓縮包,其中包含一堆XML文件,資產和類文件。javascript 解析 APK 文件信息,要做的就是先解壓,然后讀取其中相關的文件,就能得到文件信息了。
難點在解壓上,參考的基本都需要借助 node 環境。由於現在維護的系統是基於 jquery 環境的。所以最終采用了
前端解析ipa、apk安裝包信息 —— app-info-parser 該文的方案,很好的解決了問題。在此非常感謝該作者。
// 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('文件解析錯誤,請重新上傳');
});
說明:
- 由於 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;
}
}
};
為了避免頁面其它錯誤,導致腳本無法運行,因此做了頁面刷新。
2. 拖拽上傳,點擊上傳和拖拽上傳綁定到一起
在做這個功能前,想到拖拽上傳可以利用 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
,默認 2Mupload_max_filesize = 100M
,默認 8M- ps:參考PHP上傳大小限制修改
- 最后點擊安裝目錄下的(
C:\xampp
)的 xampp.control.exe 打開界面,在打開界面中,將 Apache 對應的 Actions 開啟 - 在瀏覽器窗口輸入
http://localhost/jq-drag-upload-apk-parse/index.html
- 即可完整查看 demo 效果
- 源碼地址