技術棧
-
jquery -
文件上傳:jquery.fileupload,github 文檔 -
apk 文件解析:app-info-parser,github 文檔
支持功能
點擊或拖拽上傳 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];
}
}
})
},
回覆【乾貨】獲取精選乾貨視頻教程
回覆【加羣】加入疑難問題攻堅交流羣
回覆【mat】獲取內存溢出問題分析詳細文檔教程
回覆【賺錢】獲取用java寫一個能賺錢的微信機器人
回覆【副業】獲取程序員副業攻略一份
點個“在看”表示朕
已閱
本文分享自微信公衆號 - 俠夢的開發筆記(xmdevnote)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。