配套视频:https://www.bilibili.com/video/BV1oA411B7gv/
背景
今天鼓捣了一下手机投屏到笔记本,就想录个视频展示一下学习成果,正好就想起了很早之前实现的这个功能。
H5文件下载是一个很简单的功能,但是把这个H5放在安卓版微信打开,功能就不能用了,因为安卓端的微信内置浏览器拦截了所有下载文件的请求。
即使微信的sdk也没有提供直接保存文件的接口,所以出路只有一条,就是跳到第三方应用进行下载,比如跳到手机浏览器、跳到微信小程序。如果是上架了应用宝的app,可以跳转应用宝下载。
之所以屏蔽,应该是H5无法监管的原因,但是不能理解的是,ios端的微信是可以下载的,难道苹果手机高人一等?
解决方案收集
-
1.微信公众号sdk(无法实现)
- 可能以前有这个功能,但是现在确实是没有了,找不到这种接口
- 微信公众号sdk官方文档:附录2-所有 JS 接口列表
-
2.跳转第三方应用
- 2.1.跳转小程序(没有实践过,但是跳转小程序,还不如跳转手机浏览器呢)
- 参考:在微信浏览器打开 H5,居然无法一键下载图片?
- 2.2.第三方应用生成的链接可以直接触发跳转外部浏览器选择窗口(骗人的吧)
-
参考:成功解决微信跳转到手机默认浏览器下载
-
这些网站都打不开了,不靠谱
-
- 2.3.如果是app,可以跳腾讯出品的应用宝下载
- 参考:H5在微信下载app
- 2.4.前端写弹窗提示或是遮罩提示,引导用户在右上角通过浏览器打开
- 参考:2022-12-06 uniApp H5端实现下载文件(包含微信浏览器内处理)
- 参考:微信H5保存或下载视频到本地,将视频直接分享视频给好友
- 参考:微信跳转手机默认浏览器提示 微信h5页面中下载第三方app的方法
- 2.1.跳转小程序(没有实践过,但是跳转小程序,还不如跳转手机浏览器呢)
最终选择的解决方案
-
想到这个方案,是一个意外。
-
一开始我只测试了zip的下载,确实不能下载,以至于我以偏概全地以为所有格式都不能下载,所以就转到百度上找答案。
-
然后测试跟我说,ios的文件有些也不能预览,不能下载。
-
所以我就丢下这个坑,先去解决ios的问题。
-
百度发现ios也是伪下载,它是先以预览的方式打开文件,需要用户点击右上角手动保存。
-
而且文件后缀和响应头
content-type
要严格对应,不对应就会报错,预览不了 -
参考:解决移动端H5下载文件提示文件类型无法识别或非法文件的问题
-
改完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;
@RestController
public class ApiController {
// 获取日志对象 Spring Boot 中内置了日志框架 Slf4j
private static Logger log = LoggerFactory.getLogger(ApiController.class);
/**
* 处理微信文件下载
* 欺骗安卓微信唤起打开外部浏览器的选择框
* ios微信可以预览每种格式的文件,但是不支持直接下载,需要用户在预览页点右上角手动保存
* 另外,ios对content_type要求严格,如果文件后缀和content_type对不上,连预览页都进不了
* 企业微信不用做任何处理
*/
@GetMapping("downloadFileWx")
public void downloadFileWx(@RequestParam String path, HttpServletRequest request, HttpServletResponse response) throws Exception {
responseOutputFileWx(path, null, request, response);
}
/**
* 响应文件流
* @param path 文件路径
* @param outputFileName 文件名称,赋值给响应头Content-Disposition
* @param request
* @param 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();
}
}
/**
* 对字符串(文件名或路径)进行url编码
*/
private String encodeStr(String str) throws Exception {
return URLEncoder.encode(str, "UTF-8");
}
/**
* 将文件转为byte数组
*/
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;
}
/**
* 是否从微信请求,需要排除企业微信
* 安卓或ios
*/
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” 打开设置,设置–>系统–>投影到此电脑
-
投影到此电脑中显示灰色不可选,或显示“我们正在确认这项功能”
-
第一次需要安装 无线显示器
-
手机使用电脑自带功能进行投屏
-
手机投屏到笔记本之后,笔记本会被劫持,就是只能操作手机画面,鼠标移不出来,可以在电脑上用鼠标直接操作手机。
-
这一点,有点不方便,比如想一边写代码,一边预览手机效果,就不能实现。
-
另外,建议选择 仅第一次 需要验证,我第一次投成功了,关闭之后,就死活投不上去,主要是笔记本不能弹出确认对话框文章来源:https://www.toymoban.com/news/detail-403217.html
-
之后,重启电脑才能第二次投屏成功。文章来源地址https://www.toymoban.com/news/detail-403217.html
方式2:幕享 软件
- 官网下载页
- 官方使用教程:如何使用幕享Windows版
- 我是在这个分享视频里面找到的这个软件:需要手机投屏电脑?这五款软件就够了!
- 这是纯投屏软件,不能在笔记本上操作手机。对手机录屏,然后传输到笔记本上,局域网下延迟不高。
到了这里,关于安卓端微信H5下载文件处理:让微信自动弹起跳转外部浏览器窗口的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!