由于产品需要给用户批量发送文件的功能,恰巧又是第一次接触小程序下载,也有很多坑,于是写了此篇文章供自己学习
目录
[路 0:让用户在系统文件管理器看到 ZIP](#路 0:让用户在系统文件管理器看到 ZIP)
[路 1:分享给好友或文件传输助手,窃取微信原生果实(用户接受不了)](#路 1:分享给好友或文件传输助手,窃取微信原生果实(用户接受不了))
[路 2:放弃「让用户在系统文件管理器看到 ZIP」](#路 2:放弃「让用户在系统文件管理器看到 ZIP」)
[路 3:只适配 电脑版微信](#路 3:只适配 电脑版微信)
[路 4:引导用户前往浏览器下载即可](#路 4:引导用户前往浏览器下载即可)
另外感谢csdn的自动保存功能,由于下班着急走直接关电脑了,第二天发现文章空白,然后突然飘来历史草稿几个字哈哈哈
前置条件
思路很明确就是要把文件下载到用户手机上,让其能在手机中查看(后来想一想,用户也没必要在在自己手机中文件管理中查看啊,直接在小程序内浏览,但无疑会加重小程序的缓存)
首先先了解下前置条件,小程序是不允许将文件随意下载到手机系统文件内的,为了避免恶意攻击,文件只能存放在小程序的沙箱中,也是为了避免重复发送网络请求获取文件。
那么zip压缩包就无法直接被下载到用户手机中,微信原生中Android是可以通过主动保存zip到文件管理中的,但ios就行不通了
由于经过一天的努力也没有看到体面一点的下载方式,经过多次逼问豆包更是得到如下回复:

但是!!!!!!!!!!!!!!!!!!!!!!!!!!!
正所谓上有政策下有对策,欢迎广大程序员用你们的秘方砸死我
实现思路
路 0:让用户在系统文件管理器看到 ZIP
一开始通过文档,以为直接保存到文件即可像微信那样将文件存放到文件管理中,但经过实践发现getFileSystemManager().saveFile的保存文件并非保存到系统盘,而是会将文件保存到小程序沙箱中,虽可通过Android/data/com.tencent.mm/MicroMsg/wxanewfiles找到被保存的文件,但这种方式对用户来讲太糟糕了,也仅限安卓用户。

javascript
const res = await uni.downloadFile({
url: zipUrl
});
const fs= uni.getFileSystemManager();
fs.saveFile({
tempFilePath:res.tempFilePath,
success:(suc)=>{
console.log(suc)
uni.showToast({
title: "下载成功"+suc.savedFilePath,
icon: "none"
});
},
fail:(fail)=>{
uni.showToast({
title: "下载失败,请稍后重试",
icon: "none"
});
}
})
路 1:分享给好友或文件传输助手,窃取微信原生果实(用户接受不了)
通过分享功能发送到微信中,在微信中打开zip即可美美预览下载
javascript
wx.shareFileMessage({
filePath: res.tempFilePath,
success: (suc) => {
console.log(suc)
},
fail(fail) {
console.log(fail)
}
});
路 2:放弃「让用户在系统文件管理器看到 ZIP」
小程序里正常走:下载 → 私有沙盒能用小程序内部解压、内部浏览,但用户自己在文件管理找不到适合不需要用户导出、只需要内部使用的场景。
通过wx.downloadFile拿到压缩包的临时路径,然uni.getFileSystemManager()获取文件管理器使用unzip进行解压,解压后的数据格式为文件名1,文件名2
javascript
// 主动解压文件,用户手动下载解压后的文件
const downLoadFile = () => {
wx.downloadFile({
url: zipUrl,
success(res) {
if (res.statusCode === 200) {
const tempPath = res.tempFilePath; // 临时路径
// 解压文件
const fs = uni.getFileSystemManager(); //获取文件管理器
fs.unzip({
zipFilePath: tempPath,
targetPath: uni.env.USER_DATA_PATH + "/unzip", // 解压目录
success: () => {
console.log("✅ 解压成功");
fs.readdir({
dirPath: `${uni.env.USER_DATA_PATH}/unzip`,
success(result) {
console.log("📂 解压出来的文件:", result.files);
fileList.value = result.files
// files 就是所有文件名数组
// 例如:["logo.png","info.txt","demo.jpg"]
}
});
},
fail: (err) => {
console.log("❌ 解压失败", err);
},
});
}
},
fail() {
wx.showToast({
title: "下载失败",
icon: "none"
});
}
});
}
将文件存放到沙箱的unzip目录内,然后通过文件名获取这些文件,判断其类型后用对应的api打开
javascript
//打开文件
const openFile = (fileName) => {
const fullPath = `${uni.env.USER_DATA_PATH}/unzip/${fileName}`;
// 获取文件后缀
const ext = fileName.split('.').pop().toLowerCase();
// 图片类型 → 用 previewImage 打开
if (['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'].includes(ext)) {
uni.previewImage({
urls: [fullPath]
});
}
// 文档类型 → 用 openDocument
else if (['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'].includes(ext)) {
uni.openDocument({
filePath: fullPath
});
}
// 其他格式 → 提示不支持
else {
uni.showToast({
title: '不支持打开此类型',
icon: 'none'
});
}
}
路 3:只适配 电脑版微信
用 wx.saveFileToDisk(Object object)电脑端可以弹出另存为,存到电脑本地,完美能用手机安卓 /iOS 直接放弃原生保存
javascript
wx.saveFileToDisk({
filePath: `${wx.env.USER_DATA_PATH}/hello.txt`,
success(res) {
console.log(res)
},
fail(res) {
console.error(res)
}
})
tip:使用uni.saveImageToPhotosAlbum或保存视频的api 也是可以保存zip到本地的,但也只有微信pc端小程序可以实现
路 4:引导用户前往浏览器下载即可
emmm...帮用户复制下网址吧
javascript
uni.setClipboardData({
data: zipUrl,
success: () => {
uni.showModal({
title: "请用浏览器下载",
content: "已复制链接,打开 Safari/Chrome 粘贴即可下载ZIP",
showCancel: false
});
}
});
ok,接下来将相对完美的方案
将文件分为zip和 (文档,图片,视频) 两类,允许用户预览和下载,
一、当用户点击zip文件时
进行设备信息判断,如果时pc端小程序则直接使用思路3,如果为手机端小程序则提供两个选择,分别为思路1和思路4

这里需要注意的是,一定要做出选项后再去下载zip到沙箱,再进行转发不然用户一直点取消你不炸了么
二、当用户点击文档图片视频时
拿到文件的后缀判断文件类型,分别调用对应的api即可,下载也是一样的
预览时图片和视频是允许使用网络路径的,但文档预览时必须使用临时地址或者沙箱内地址
有点击就有可能会出现并发,可以通过状态锁(跟防抖不太一样,毕竟用户一直点,你就一直不给下载,他就一直点,你就一直不给下载,他就一直点,你就一直不给下载..............................................................)和遮罩来避免,思路:定义一个状态,只要当前有文件正在准备打开,就拦截所有的后续点击。当用户点击下载后弹出遮罩层禁止用户进行傻瓜操作
最后无论下载成功还是失败记得流程结束后把状态锁打开!!!
代码如下
javascript
const zipFileList = ref([])
// 下载压缩包
const downLoadZip = async (url) => {
const sys = uni.getSystemInfoSync(); //判断用户为ios还是andrion
if (["android", "devtools", "ios", "ohos"].includes(sys.platform)) {
uni.showActionSheet({
itemList: ["发送给文件传输助手", "复制链接到浏览器下载"],
success: function(res) {
if (res.tapIndex == 0) {
uni.downloadFile({
url,
success(res) {
if (res.statusCode === 200) {
wx.shareFileMessage({
filePath: res.tempFilePath // 下载后的临时路径
})
} else {
uni.showToast({
title: '文件损坏',
icon: 'none'
});
}
},
fail: function(res) {
uni.showToast({
title: '请稍后重试',
icon: 'none'
});
}
})
} else {
uni.setClipboardData({
data: url,
success: () => {
uni.showModal({
title: "请用浏览器下载",
content: "已复制链接,请打开手机自带浏览器(如Safari/Chrome)粘贴网址进行下载",
showCancel: false
});
}
});
}
}
})
} else if (["windows", "mac"].includes(sys.platform)) {
uni.downloadFile({
url,
success(res) {
if (res.statusCode === 200) {
wx.saveFileToDisk({
filePath: res.tempFilePath,
success() {
wx.showToast({
title: '保存成功'
});
},
fail(error) {
uni.showToast({
title: "保存失败",
icon: "none"
})
}
})
} else {
uni.showToast({
title: '文件损坏',
icon: 'none'
});
}
},
fail: function(res) {
uni.showToast({
title: '文件损坏',
icon: 'none'
});
}
})
} else {
uni.setClipboardData({
data: url,
success: () => {
uni.showModal({
title: "请用浏览器下载",
content: "已复制链接,请打开手机自带浏览器(如Safari/Chrome)粘贴网址进行下载",
showCancel: false
});
}
});
}
}
// 打开文件
const previewMyFile = async (fileUrl) => {
// 完整网络地址
const fullNetworkUrl = serverUrl + fileUrl;
// 文件名
const fileName = fileUrl.split('/')[fileUrl.split('/').length - 1]
// 获取文件后缀
const ext = fileName.split('.').pop().toLowerCase();
// 1.压缩包
if (['zip', 'rar', '7z'].includes(ext)) {
downLoadZip(fullNetworkUrl);
return;
}
// 2. 图片网络直开
if (['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'].includes(ext)) {
uni.previewImage({
urls: [fullNetworkUrl]
});
return;
}
if (['mp4', 'mov'].includes(ext)) {
uni.previewMedia({
sources: [{
url: fullNetworkUrl,
type: "video"
}]
});
return;
}
// 3. 文档类:必须走 沙箱缓存 逻辑
if (['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'].includes(ext)) {
if (isHandlingPreview) return; // 【预览锁】生效
isHandlingPreview = true;
uni.showLoading({
title: '正在打开...',
mask: true
});
try {
const fs = uni.getFileSystemManager();
const dirPath = `${uni.env.USER_DATA_PATH}/myFile`;
const fullPath = `${dirPath}/${fileName}`;
// 第一步:查沙箱缓存
try {
fs.accessSync(fullPath);
// 有缓存,直接打开,结束流程
uni.openDocument({
filePath: fullPath,
showMenu: true
});
return;
} catch (e) {
// 无缓存,继续往下执行下载
}
// 第二步:沙箱无缓存,下载临时文件
const res = await uni.downloadFile({
url: fullNetworkUrl
});
if (res.statusCode === 200) {
try {
// 建目录,存沙箱
try {
fs.accessSync(dirPath);
} catch (err) {
fs.mkdirSync(dirPath, true);
}
fs.saveFileSync(res.tempFilePath, fullPath);
// 打开沙箱文件
uni.openDocument({
filePath: fullPath,
showMenu: true
});
} catch (saveErr) {
// 兜底:存沙箱失败,用临时路径直接打开
uni.openDocument({
filePath: res.tempFilePath,
showMenu: true
});
}
} else {
uni.showToast({
title: '文件读取失败',
icon: 'none'
});
}
} catch (err) {
uni.showToast({
title: '请稍后重试',
icon: 'none'
});
} finally {
uni.hideLoading();
setTimeout(() => {
isHandlingPreview = false;
}, 300); // 解锁
}
return;
}
// 4. 其他未知格式
uni.showToast({
title: '不支持预览此格式,请尝试下载',
icon: 'none'
});
}
// 下载文件
const downloadToPhone = async (fileUrl) => {
// 完整网络地址
const fullNetworkUrl = serverUrl + fileUrl;
// 文件名
const fileName = fileUrl.split('/')[fileUrl.split('/').length - 1]
// 获取文件后缀
const ext = fileName.split('.').pop().toLowerCase();
// 1.压缩包
if (['zip', 'rar', '7z'].includes(ext)) {
downLoadZip(fullNetworkUrl);
return;
}
if (isHandlingDownload) return; // 生效
isHandlingDownload = true;
uni.showLoading({
title: '正在下载...',
mask: true
});
try {
const fs = uni.getFileSystemManager();
const dirPath = `${uni.env.USER_DATA_PATH}/myFile`;
const fullPath = `${dirPath}/${fileName}`;
let isFileExist = true;
// 第一步:查沙箱缓存
try {
fs.accessSync(fullPath);
isFileExist = true;
} catch (e) {
isFileExist=false
}
if(!isFileExist){
// 第二步:沙箱无缓存,下载临时文件
const res = await uni.downloadFile({
url: fullNetworkUrl
});
if (res.statusCode === 200) {
try {
// 建目录,存沙箱
try {
fs.accessSync(dirPath);
} catch (err) {
fs.mkdirSync(dirPath, true);
}
fs.saveFileSync(res.tempFilePath, fullPath);
} catch (saveErr) {
// 如果沙箱存不进去直接下载res.tempFilePath
// 1.直接下载
if (['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'].includes(ext)) {
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath
})
return;
}
if (['mp4', 'mov'].includes(ext)) {
uni.saveVideoToPhotosAlbum({
filePath: res.tempFilePath
})
return;
}
// 2.无法直接下载 文档类
if (['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'].includes(ext)) {
uni.openDocument({
filePath: res.tempFilePath,
showMenu: true
});
}
return;
}
} else {
uni.showToast({
title: '请稍后重试',
icon: 'none'
});
}
}
// 1.直接下载
if (['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'].includes(ext)) {
uni.saveImageToPhotosAlbum({
filePath: fullPath
})
return;
}
if (['mp4', 'mov'].includes(ext)) {
uni.saveVideoToPhotosAlbum({
filePath: fullPath
})
return;
}
// 2.无法直接下载 文档类
if (['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'].includes(ext)) {
uni.openDocument({
filePath: fullPath,
showMenu: true
});
}
} catch (err) {
uni.showToast({
title: '请稍后重试',
icon: 'none'
});
} finally {
uni.hideLoading();
setTimeout(() => {
isHandlingDownload = false;
}, 300); // 解锁
}
return;
}