配套视频:https://www.bilibili.com/video/BV1oA411B7gv/
背景
今天鼓捣了一下手机投屏到笔记本,就想录个视频展示一下学习成果,正好就想起了很早之前实现的这个功能。
H5文件下载是一个很简单的功能,但是把这个H5放在安卓版微信打开,功能就不能用了,因为安卓端的微信内置浏览器拦截了所有下载文件的请求。
即使微信的sdk也没有提供直接保存文件的接口,所以出路只有一条,就是跳到第三方应用进行下载,比如跳到手机浏览器、跳到微信小程序。如果是上架了应用宝的app,可以跳转应用宝下载。
之所以屏蔽,应该是H5无法监管的原因,但是不能理解的是,ios端的微信是可以下载的,难道苹果手机高人一等?
解决方案收集
-
1.微信公众号sdk(无法实现)
- 可能以前有这个功能,但是现在确实是没有了,找不到这种接口
- 微信公众号sdk官方文档:附录2-所有 JS 接口列表
-
2.跳转第三方应用
- 2.1.跳转小程序(没有实践过,但是跳转小程序,还不如跳转手机浏览器呢)
- 2.2.第三方应用生成的链接可以直接触发跳转外部浏览器选择窗口(骗人的吧)
-
这些网站都打不开了,不靠谱
- 2.3.如果是app,可以跳腾讯出品的应用宝下载
- 参考:H5在微信下载app
- 2.4.前端写弹窗提示或是遮罩提示,引导用户在右上角通过浏览器打开
最终选择的解决方案
-
想到这个方案,是一个意外。
-
一开始我只测试了zip的下载,确实不能下载,以至于我以偏概全地以为所有格式都不能下载,所以就转到百度上找答案。
-
然后测试跟我说,ios的文件有些也不能预览,不能下载。
-
所以我就丢下这个坑,先去解决ios的问题。
-
百度发现ios也是伪下载,它是先以预览的方式打开文件,需要用户点击右上角手动保存。
-
而且文件后缀和响应头
content-type
要严格对应,不对应就会报错,预览不了 -
改完ios的问题,我传了各种格式的文件测试了一遍,确认修复之后,又转回安卓端。
-
随手点击了几下,就是这么几下让我看到了希望。
-
并不是所有类型的文件都不能下载,针对docx、pdf、xlsx、txt等格式,微信会主动唤起跳转其他浏览器的选择弹窗。
-
这比起前端写提示窗无疑要友好许多。
-
所以只要发挥偷蒙拐骗的优良品质,让微信对所有文件一视同仁,都唤起跳转窗口就行了。
-
到此,安卓端H5下载文件的问题完美解决。
-
欺骗的手段也很简单,反正微信也不能下载,就所有的下载请求,都给它一个假文件,比如123456.xlsx。
java实现
- 注意,如果接口使用cookie鉴权,跳转外部浏览器,cookie是带不过去的。
- 需要提供一个不需要鉴权的接口,换一种方式鉴权,比如时效分享码或者直接携带sessionId之类的
import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.*;import org.springframework.web.context.WebApplicationContext;import org.springframework.web.method.HandlerMethod;import org.springframework.web.servlet.mvc.method.RequestMappingInfo;import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;import javax.servlet.ServletContext;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.File;import java.io.FileInputStream;import java.io.IOException;import java.io.OutputStream;import java.net.URLEncoder;import java.text.MessageFormat;import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map;@RestControllerpublic class ApiController { // 获取日志对象 Spring Boot 中内置了日志框架 Slf4j private static Logger log = LoggerFactory.getLogger(ApiController.class); @GetMapping("downloadFileWx") public void downloadFileWx(@RequestParam String path, HttpServletRequest request, HttpServletResponse response) throws Exception { responseOutputFileWx(path, null, request, response); } public void responseOutputFileWx(String path, String outputFileName, HttpServletRequest request, HttpServletResponse response) throws Exception { File file = new File(path); if (file == null || !file.exists() || !file.isFile()) { log.error("文件不存在"); // 重定向到当前页面,相当于刷新页面 String contextPath = request.getContextPath(); response.sendRedirect(contextPath + "/downFile"); return; } if (outputFileName == null || outputFileName.trim().length() == 0) { // 假如下载文件名参数为空,则设置为原始文件名 outputFileName = file.getName(); } ServletContext context = request.getServletContext(); // 文件绝对路径 String absolutePath = file.getAbsolutePath(); // 获取文件的MIME type String mimeType = context.getMimeType(absolutePath); if (mimeType == null) { // 没有发现则设为二进制流 mimeType = "application/octet-stream"; } response.setContentType(mimeType); // 设置文件下载响应头 String headerKey = "Content-Disposition"; String headerValue = null; if (isWx(request)) { // 微信浏览器,打开手机默认浏览器下载文件 // 注意排除企业微信 try { if (isAndroidWx(request)) { // 安卓端,xlsx文件类型会触发微信弹出跳转外部浏览器窗口,欺骗一下 response.setContentType("application/octet-stream"); outputFileName = "123456.xlsx"; } else { // ios 微信对contentType要求比较严格 // https://juejin.cn/post/6844904086463053837 if (absolutePath.endsWith("xlsx")) { response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); } else if (absolutePath.endsWith("xls")) { response.setContentType("application/vnd.ms-excel"); } else if (absolutePath.endsWith("doc")) { response.setContentType("application/msword"); } else if (absolutePath.endsWith("docx")) { response.setContentType("application/application/vnd.openxmlformats-officedocument.wordprocessingml.document"); } } headerValue = String.format("attachment; filename=\"%s\"", URLEncoder.encode(outputFileName, "UTF-8")); } catch (Exception e) { headerValue = String.format("attachment; filename=\"%s\"", outputFileName); log.error(e.getMessage(), e); } } else { try { // 解决Firefox浏览器中文件名中文乱码 // https://blog.csdn.net/Jon_Smoke/article/details/53699400 headerValue = String.format("attachment; filename* = UTF-8''%s", URLEncoder.encode(outputFileName, "UTF-8") ); } catch (Exception e) { headerValue = String.format("attachment; filename=\"%s\"", outputFileName); log.error(e.getMessage(), e); } } response.setHeader(headerKey, headerValue); String fileName = file.getName(); try (OutputStream outputStream = response.getOutputStream()) { response.setCharacterEncoding("utf-8"); // 将下面2行放开,可以测试微信最原始反应 // 设置返回类型 // response.setContentType("multipart/form-data"); // // 文件名转码一下,不然会出现中文乱码 // response.setHeader("Content-Disposition", "attachment;fileName=" + encodeStr(fileName)); byte[] bytes = readBytes(file); if (bytes == null) { log.error("文件不存在"); } outputStream.write(bytes); log.info("文件下载成功!" + fileName); } catch (Exception e) { e.printStackTrace(); } } private String encodeStr(String str) throws Exception { return URLEncoder.encode(str, "UTF-8"); } public byte[] readBytes(File file) throws Exception { long len = file.length(); // 无论数组的类型如何,数组中的最大元素数为Integer.MAX_VALUE,大约20亿 if (len >= 2147483647L) { return null; } else { byte[] bytes = new byte[(int) len]; try (FileInputStream in = new FileInputStream(file)) { int readLength = in.read(bytes); if ((long) readLength < len) { log.error("文件未读取完全"); return null; } } catch (Exception var10) { return null; } return bytes; } } private static boolean isAndroidWx(HttpServletRequest request) { String userAgent = request.getHeader("user-agent"); return userAgent != null && userAgent.toLowerCase().indexOf("micromessenger") > -1 && userAgent.toLowerCase().indexOf("wxwork") < 0 && userAgent.toLowerCase().indexOf("android") > -1; } private static boolean isWx(HttpServletRequest request) { String userAgent = request.getHeader("user-agent"); return userAgent != null && userAgent.toLowerCase().indexOf("micromessenger") > -1 && userAgent.toLowerCase().indexOf("wxwork") < 0; }}
题外话:手机如何投屏笔记本
方式1:win10自带投屏
-
按 “Windows 徽标键+I” 打开设置,设置–>系统–>投影到此电脑
-
第一次需要安装 无线显示器
-
手机投屏到笔记本之后,笔记本会被劫持,就是只能操作手机画面,鼠标移不出来,可以在电脑上用鼠标直接操作手机。
-
这一点,有点不方便,比如想一边写代码,一边预览手机效果,就不能实现。
-
另外,建议选择 仅第一次 需要验证,我第一次投成功了,关闭之后,就死活投不上去,主要是笔记本不能弹出确认对话框
-
之后,重启电脑才能第二次投屏成功。
方式2:幕享 软件
- 官网下载页
- 官方使用教程:如何使用幕享Windows版
- 我是在这个分享视频里面找到的这个软件:需要手机投屏电脑?这五款软件就够了!
- 这是纯投屏软件,不能在笔记本上操作手机。对手机录屏,然后传输到笔记本上,局域网下延迟不高。
来源地址:https://blog.csdn.net/weixin_44174211/article/details/128985936