技术栈

参考:前端解析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 该文的方案,很好的解决了问题。在此非常感谢该作者。

  1. // apk 文件解析
  2. var parser = new AppInfoParser(data.files[0]);
  3. parser.parse().then(function(result) {
  4. uploadMod.doms.uploadErr.html('');
  5. var appInfo = result.application || {};
  6. var formAppInfo = {
  7. name: appInfo.label ? (Array.isArray(appInfo.label) ? appInfo.label[0] : appInfo.label) : '',
  8. package: result.package,
  9. version: result.versionName,
  10. version_code: result.versionCode
  11. };
  12. // 省略其他操作代码...
  13. }).catch(function (err) {
  14. uploadMod.doms.uploadErr.html('文件解析错误,请重新上传');
  15. });

说明:

  • 由于 app-info-parser 底层用了 async 语法,在 IE 下是不兼容的。在 firefox、chrome 下是正常的。
  • 上传假 APK 文件,不能处理,js 脚本会报错:File format is not recognized.。目前想到的解决方案是 js 监听错误,然后进行处理。若有更好想法的,欢迎@我。在此提前感谢。
  1. // console.error() 监控处理
  2. consoleError = window.console.error;
  3. window.console.error = function () {
  4. consoleError && consoleError.apply(window, arguments);
  5. for (var info in arguments) {
  6. if (arguments[info] == 'File format is not recognized.') {
  7. $('#app_parse').html('<p style="color:red;">由于您上传了非真正的 APK 文件,导致脚本解析出错,即将重新刷新页面,给您带来不好的体验,敬请原谅</p>');
  8. setTimeout(function () {
  9. history.go(0);
  10. }, 3000);
  11. return false;
  12. }
  13. }
  14. };

为了避免页面其它错误,导致脚本无法运行,因此做了页面刷新。

2. 拖拽上传,点击上传和拖拽上传绑定到一起

在做这个功能前,想到拖拽上传可以利用 H5 的拖拽功能及原生 js 的 file 文件上传实现,但需要处理兼容性问题。后来想到系统中已经引入了 jquery.fileupload 库,于是特地翻阅了文档,支持拖拽上传。因此采用该库实现拖拽上传功能。

html 布局如下:

  1. <div class="upload-area" id="upload_area">
  2. <i class="icon-upload"></i>
  3. <p class="upload-text">将安装包拖拽至此上传或 <em>选择文件</em></p>
  4. <p class="upload-tip">支持 APK 文件,最大不超过 300 MB</p>
  5. <input type="file" id="upload_input" name="file" accept="application/vnd.android.package-archive" data-size="300"/>
  6. </div>

如何将 拖拽、点击 一起处理,用一个上传方法实现,而不是分开需要实现2遍?

想法是,点击外层容器,触发 input 点击事件。前提是需要实现 input 点击事件,并且阻止冒泡事件,因为外层也有点击事件。

  1. $('body').on('click', '#upload_input', function (e) {
  2. e.stopPropagation();
  3. uploadMod.methods.fileUpload();
  4. }).on('click drop dragenter dragover dragleave', '#upload_area', function(e) {
  5. e.preventDefault();
  6. uploadMod.doms.uploadErr.html('');
  7. switch (e.type) {
  8. case 'click':
  9. $('#upload_input').val(null);
  10. $('#upload_input').click();
  11. break;
  12. case 'drop':
  13. uploadMod.doms.uploadArea.removeClass('active');
  14. $('#upload_input').val(null);
  15. uploadMod.methods.fileUpload();
  16. break;
  17. case 'dragenter':
  18. case 'dragover':
  19. uploadMod.doms.uploadArea.addClass('active');
  20. break;
  21. case 'dragleave':
  22. uploadMod.doms.uploadArea.removeClass('active');
  23. break;
  24. }
  25. })

实现了拖拽进入高亮、远离恢复。需要注意的是,$('#upload_input') 不能用缓存的变量。否则会导致二次点击上传失效,无法触发点击打开文件窗口。以及此时拖拽上传一个正确的文件,会触发 2 次文件上传。发送 2 次上传接口。感兴趣的朋友可以自己用缓存的试一下。

案例复现:

  • 点击假的内容为空的 apk 文件,会提示:文件尺寸不对。
  • 此时,第二次点击,无法触发 input 的点击事件。反复多次依然无效。
  • 此时,通过拖拽上传,能够正常执行,但是会触发 2 次上传处理,解析 2 次文件,发送 2 次上传接口请求。

3. 在上传之前不知道 APK 文件信息,需要执行上传操作过程中将解析的文件信息作为参数放到上传接口中

之前做过的上传,是在上传前就已经知道在上传时需要提交的额外参数值。

  1. $('#upload_input').fileupload({
  2. url: 'http://localhost:80/jq-drag-upload-apk-parse/upload.php',
  3. dataType: 'json',
  4. formData: params, // params 为 js 对象,是需要提交的参数
  5. multi: false,
  6. // 省略....
  7. })

但现在,在上传前是不知道参数值的,需要在执行上传操作,拿到上传文件信息,并解析出上传文件的信息,然后将解析信息做为参数值放到上传请求中。那怎么做呢,研究了很久,才找到。

  1. $('#upload_input').fileupload({
  2. url: 'http://localhost:80/jq-drag-upload-apk-parse/upload.php',
  3. dataType: 'json',
  4. formData: params, // params 为 js 对象,是需要提交的参数
  5. multi: false,
  6. add: function (e, data) {
  7. // 省略文件类型及大小校验
  8. // 省略 APK 文件解析及进度条等的 UI 初始化
  9. $(e.target).fileupload(
  10. 'option',
  11. 'formData',
  12. formAppInfo // APK 解析出的数据
  13. );
  14. data.submit();
  15. },
  16. // 省略....
  17. })

4. 上传过程中取消上传

这个相对比较容易。利用上传回调中的 data.abort() 即可实现。需要处理的是,在 add() 方法里需要先在外层缓存一下 data,才方便对其的调用。

  1. $('#upload_input').fileupload({
  2. url: 'http://localhost:80/jq-drag-upload-apk-parse/upload.php',
  3. dataType: 'json',
  4. formData: params, // params 为 js 对象,是需要提交的参数
  5. multi: false,
  6. add: function (e, data) {
  7. // 省略文件类型及大小校验
  8. // 省略 APK 文件解析及进度条等的 UI 初始化
  9. // 外层缓存,方便调取消上传
  10. uploadMod.uploadXHR = data;
  11. $(e.target).fileupload(
  12. 'option',
  13. 'formData',
  14. formAppInfo // APK 解析出的数据
  15. );
  16. data.submit();
  17. },
  18. fail: function(e, data) {
  19. if (data.errorThrown == 'abort') {
  20. uploadMod.doms.uploadErr.html('已取消上传,可重新上传');
  21. } else {
  22. uploadMod.doms.uploadErr.html('上传失败,请重新上传');
  23. }
  24. },
  25. // 省略....
  26. })
  1. $('body').on('click', '#upload_cancel', function () {
  2. uploadMod.uploadXHR.abort();
  3. })

5. 文件上传的主要代码

  1. fileCheck: function(e, data) {
  2. // 文件格式及文件大小校验
  3. var acceptFileTypes = uploadMod.doms.uploadInput.attr('accept');
  4. var supportFileTypes = ['apk']; // 通过name后缀再校验一次,避免获取不到type的情况
  5. var maxSize = uploadMod.doms.uploadInput.data('size') * 1024 * 1024; // 单位mb,需要转换为b
  6. var fileTypeFlag = data.originalFiles.every(function(item) {
  7. if (item.type) {
  8. return acceptFileTypes.indexOf(item.type) > -1;
  9. } else {
  10. var splits = (item.name || file).split('.');
  11. var fileType = splits[splits.length - 1];
  12. return supportFileTypes.indexOf(fileType) > -1;
  13. }
  14. });
  15. if (!fileTypeFlag) {
  16. uploadMod.doms.uploadErr.html('请上传 APK 文件');
  17. return false;
  18. }
  19. var fileSizeFlag = data.originalFiles.every(function(item) {
  20. return item.size > 0 && item.size <= maxSize;
  21. });
  22. if (!fileSizeFlag) {
  23. data = {};
  24. uploadMod.doms.uploadErr.html('文件大小不正确');
  25. return false;
  26. }
  27. uploadMod.doms.progressWrap.show();
  28. var $appParse = uploadMod.doms.progressWrap.find('.app-parse'),
  29. $progressCon = uploadMod.doms.progressWrap.find('.con');
  30. $appParse.show();
  31. $progressCon.hide();
  32. // apk 文件解析
  33. var parser = new AppInfoParser(data.files[0]);
  34. parser.parse().then(function(result) {
  35. uploadMod.doms.uploadErr.html('');
  36. var appInfo = result.application || {};
  37. var formAppInfo = {
  38. name: appInfo.label ? (Array.isArray(appInfo.label) ? appInfo.label[0] : appInfo.label) : '',
  39. package: result.package,
  40. version: result.versionName,
  41. version_code: result.versionCode
  42. };
  43. // 进度条初始化
  44. $appParse.hide();
  45. $progressCon.show();
  46. if (result.icon) {
  47. uploadMod.doms.progressWrap.find('.icon-app').css('background-image', 'url("' + result.icon + '")');
  48. }
  49. uploadMod.doms.progressWrap.find('.name').html(formAppInfo.name);
  50. uploadMod.doms.progressWrap.find('.package').html(formAppInfo.package);
  51. uploadMod.doms.progressWrap.find('.version').html(formAppInfo.version);
  52. uploadMod.doms.progressWrap.find('.version-code').html(formAppInfo.version_code);
  53. uploadMod.doms.progressWrap.find('.progress').css('width', 0);
  54. uploadMod.doms.progressWrap.find('.num').html(0);
  55. uploadMod.doms.progressWrap.find('.size').html(0);
  56. // 设置上传接口参数
  57. uploadMod.uploadXHR = data;
  58. $(e.target).fileupload(
  59. 'option',
  60. 'formData',
  61. formAppInfo
  62. );
  63. data.submit();
  64. }).catch(function (err) {
  65. uploadMod.doms.progressWrap.hide();
  66. uploadMod.doms.uploadErr.html('文件解析错误,请重新上传');
  67. data.abort();
  68. });
  69. // console.error() 监控处理
  70. consoleError = window.console.error;
  71. window.console.error = function () {
  72. consoleError && consoleError.apply(window, arguments);
  73. for (var info in arguments) {
  74. if (arguments[info] == 'File format is not recognized.') {
  75. $('#app_parse').html('<p style="color:red;">由于您上传了非真正的 APK 文件,导致脚本解析出错,即将重新刷新页面,给您带来不好的体验,敬请原谅</p>');
  76. setTimeout(function () {
  77. history.go(0);
  78. }, 3000);
  79. return false;
  80. }
  81. }
  82. };
  83. },
  84. fileUpload: function(el) {
  85. $('#upload_input').fileupload({
  86. url: 'http://localhost:80/jq-drag-upload-apk-parse/upload.php',
  87. dataType: 'json',
  88. multi: false,
  89. add: uploadMod.methods.fileCheck,
  90. paste: function () { return false; },
  91. done: function(e, data) { // 上传成功回调
  92. var result = data.result;
  93. if (result && result.flag && result.data) {
  94. uploadMod.doms.uploadErr.html(result.msg || '上传成功');
  95. uploadMod.data.selectedAPK = result.data;
  96. uploadMod.methods.renderHistory(result.data);
  97. } else {
  98. uploadMod.doms.progressWrap.hide();
  99. uploadMod.doms.uploadErr.html(result.msg || '上传失败');
  100. }
  101. },
  102. fail: function(e, data) {
  103. if (data.errorThrown == 'abort') {
  104. uploadMod.doms.uploadErr.html('已取消上传,可重新上传');
  105. } else {
  106. uploadMod.doms.uploadErr.html('上传失败,请重新上传');
  107. }
  108. },
  109. progressall: function(e, data) {
  110. var progress = parseInt(data.loaded / data.total * 100, 10);
  111. uploadMod.doms.progressWrap.find('.progress').css('width', progress + '%');
  112. uploadMod.doms.progressWrap.find('.num').html(progress);
  113. uploadMod.doms.progressWrap.find('.size').html(bytesToSize(data.bitrate));
  114. function bytesToSize(bit) {
  115. if (bit === 0) return '0 B';
  116. var bytes = bit / 8;
  117. var k = 1024,
  118. sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
  119. i = Math.floor(Math.log(bytes) / Math.log(k));
  120. return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i];
  121. }
  122. }
  123. })
  124. },

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 效果
  • 源码地址

基于 jq 实现拖拽上传 APK 文件,js解析 APK 信息的更多相关文章

  1. [开源应用]利用HTTPHandler+resumableJs+HTML5实现拖拽上传[大]文件

    前言: 大文件传输一直是技术上的一大难点.文件过大时,一些性提交所有的内容进内存是不现实的.大文件带来问题还有是否支持断点传输和多文件同时传输. 本文以resumableJs为例,介绍了如何在ASP. ...

  2. spring html5 拖拽上传多文件

    注:这仅仅是一个粗略笔记.有些代码可能没用.兴许会再更新一个能够使用的版本号.不足之处,敬请见谅. 1.spring环境搭建,这里使用的是spring3的jar,须要同一时候引入common-IO 和 ...

  3. JavaScript 文件拖拽上传插件 dropzone.js 介绍

    http://www.renfei.org/blog/dropzone-js-introduction.html

  4. dropzonejs中文翻译手册 DropzoneJS是一个提供文件拖拽上传并且提供图片预览的开源类库.

    http://wxb.github.io/dropzonejs.com.zh-CN/dropzonezh-CN/ 由于项目需要,完成一个web的图片拖拽上传,也就顺便学习和了解了一下前端的比较新的技术 ...

  5. [转]人人网首页拖拽上传详解(HTML5 Drag&Drop、FileReader API、formdata)

    人人网首页拖拽上传详解(HTML5 Drag&Drop.FileReader API.formdata) 2011年12月11日 | 彬Go 上一篇:给力的 Google HTML5 训练营( ...

  6. Nodejs express、html5实现拖拽上传

    一.前言 文件上传是一个比较常见的功能,传统的选择方式的上传比较麻烦,需要先点击上传按钮,然后再找到文件的路径,然后上传.给用户体验带来很大问题.html5开始支持拖拽上传的需要的api.nodejs ...

  7. [python][flask] Flask 图片上传与下载例子(支持漂亮的拖拽上传)

    目录 1.效果预览 2.新增逻辑概览 3.tuchuang.py 逻辑介绍 3.1 图片上传 3.2 图片合法检查 3.3 图片下载 4.__init__.py 逻辑介绍 5.upload.html ...

  8. Dropzone.js实现文件拖拽上传

    dropzone.js是一个开源的JavaScript库,提供 AJAX 异步文件上传功能,支持拖拽文件.支持最大文件大小.支持设置文件类型.支持预览上传结果,不依赖jQuery库. 使用Dropzo ...

  9. 图片上传插件ImgUploadJS:用HTML5 File API 实现截图粘贴上传、拖拽上传

    一 . 背景及效果 当前互联网上传文件最多的就是图片文件了,但是传统web图片的截图上传需要:截图保存->选择路径->保存后再点击上传->选择路径->上传->插入. 图片 ...

随机推荐

  1. Read-only file system

    mount -o remount rw /  

  2. OO第一单元三次作业总结

    写在前面 第一单元作业是针对输入的多项式进行格式合法判断,然后进行求导,结果长度优化,最后输出.三次难度递增,不断添加新的需求,总体感觉在实现方面没有多大困难(?),个人主要困扰环节是寻找自己未知bu ...

  3. Python二维数组,坑苦了

    myList = [[0] * 3] * 4 但是当操作myList[0][1] = 1时,发现整个第二列都被赋值,变成 [[0,1,0], [0,1,0], [0,1,0], [0,1,0]] my ...

  4. MySQL PROFILE 跟踪语句各阶段性能开销

    PROFILE  可以跟踪查询语句各个阶段 Time,IO,CPU,MEMORY 等资源使用情况,比较详细.所以系统一般不会记录太多.启用是全局的,所以每个连接都保持语句的资源使用情况. 查看 PRO ...

  5. spring深入学习(四)-----spring aop

    AOP概述 aop其实就是面向切面编程,举个例子,比如项目中有n个方法是对外提供http服务的,那么如果我需要对这些http服务进行响应时间的监控,按照传统的方式就是每个方法中添加相应的逻辑,但是这些 ...

  6. hbase常用操纵操作——增删改查

    查询某个资金账户的信息 get 'dmp:hbase_tags','资金账号' 创建表 create 'emp', 'personal data', 'professional data' 在HBas ...

  7. eslint 的 env 配置是干嘛使的?

    这笔修改体现了 env 和 global 的关系: https://github.com/g8up/youDaoDict/commit/8b05616f 官方文档表述: https://eslint. ...

  8. C#关于xml文件和TreeView之间的转换解析

    主窗体: using System; using System.Collections; using System.Collections.Generic; using System.Componen ...

  9. Linux更新源汇总-18.9.7更新

    企业站 阿里云:https://opsx.alibaba.com/mirror 网易:http://mirrors.163.com/ 教育站 北京理工大学:http://mirror.bit.edu. ...

  10. git安装以及初始化

    安装文档参见:https://www.cnblogs.com/ximiaomiao/p/7140456.html 注意:安装成功后,用cmd进行基本信息设置时,当出现“git不是内部或外部命令,也不是 ...