前言
在 Uniapp 跨端开发中,文件下载是高频需求(如 PDF 报表、Excel 数据、图片等),但 H5 与小程序的运行环境差异极大:小程序有独立的文件系统,而 H5 依赖浏览器原生能力,直接复用代码会出现兼容性问题。本文将详细拆解两者的实现逻辑,提供一套兼容两端的完整方案,附代码示例和避坑指南。
一、核心差异:H5 与小程序下载机制对比
|
特性 |
H5 环境 |
小程序环境(微信 / 支付宝等) |
|
核心 API |
浏览器原生 a 标签、XMLHttpRequest |
uni.downloadFile + uni.saveFile |
|
文件存储 |
无本地文件系统,需用户手动保存 |
支持临时文件 + 永久文件存储 |
|
跨域限制 |
受浏览器 CORS 政策严格限制 |
需配置合法域名,跨域由微信后台处理 |
|
进度监听 |
需通过原生 progress 事件实现 |
uni.downloadFile 自带 onProgressUpdate |
|
文件打开 |
依赖浏览器默认行为(新标签页打开) |
uni.openDocument 支持本地打开 |
二、通用前置准备(两端都需注意)
- 跨域配置:
- 小程序:登录对应平台后台(如微信公众平台),在「开发设置」中添加下载地址的合法域名(downloadFile 域名)。
- H5:后端需配置 CORS 响应头(A***ess-Control-Allow-Origin: *、A***ess-Control-Allow-Methods: GET 等),否则跨域下载失败。
- 权限说明:
- 小程序无需额外授权,H5 依赖浏览器下载权限(无需手动申请)。
- 文件格式支持:
主流格式(PDF、doc、xls、jpg、png)均支持,特殊格式(如 .zip)需结合对应解析工具。
三、分场景实现方案
场景 1:基础下载(下载后保存 / 打开文件)
3.1 小程序端实现(推荐 downloadFile + saveFile)
/**
* 小程序下载文件
* @param {String} url - 下载地址
* @param {String} fileName - 自定义文件名
*/
miniProgramDownload(url, fileName = '文件') {
uni.showLoading({ title: '下载中...' });
// 1. 下载文件到临时路径
uni.downloadFile({
url: url,
header: {
// 如需携带登录态,添加 Token(示例)
// 'Authorization': 'Bearer ' + uni.getStorageSync('token')
},
// 监听下载进度
onProgressUpdate: (res) => {
console.log(`下载进度:${res.progress}%`);
// 可结合进度条组件实时展示
},
su***ess: (downloadRes) => {
// 下载成功(状态码 200)
if (downloadRes.statusCode === 200) {
// 2. 保存文件到本地(永久路径,避免临时文件被清理)
uni.saveFile({
tempFilePath: downloadRes.tempFilePath,
su***ess: (saveRes) => {
uni.hideLoading();
uni.showToast({ title: '下载成功', icon: 'su***ess' });
// 3. 打开文件(可选,支持 PDF/文档等)
this.openFile(saveRes.savedFilePath);
},
fail: (err) => {
uni.hideLoading();
uni.showToast({ title: '保存失败', icon: 'none' });
console.error('保存失败:', err);
}
});
} else {
uni.hideLoading();
uni.showToast({ title: `下载失败:${downloadRes.statusCode}`, icon: 'none' });
}
},
fail: (err) => {
uni.hideLoading();
uni.showToast({ title: '下载失败', icon: 'none' });
console.error('下载失败:', err);
}
});
},
// 打开已下载的文件
openFile(filePath) {
uni.openDocument({
filePath: filePath,
showMenu: true, // 显示右上角菜单(支持转发、保存到手机)
su***ess: () => console.log('文件打开成功'),
fail: (err) => {
uni.showToast({ title: '打开文件失败', icon: 'none' });
console.error('打开失败:', err);
}
});
}
3.2 H5 端实现(替代方案:a 标签 + 原生下载)
由于 H5 不支持 uni.saveFile,且 uni.downloadFile 无法获取本地路径,需通过浏览器原生 a 标签触发下载:
/**
* H5 下载文件
* @param {String} url - 下载地址
* @param {String} fileName - 自定义文件名
*/
h5Download(url, fileName = '文件') {
// 处理跨域:若 URL 跨域,需后端配置 CORS 或使用代理
const link = document.createElement('a');
// 若需携带 Token,拼接在 URL 或通过 header(需后端支持)
// url = `${url}?token=${uni.getStorageSync('token')}`;
link.href = url;
link.download = fileName; // 自定义下载文件名(浏览器支持有限,部分格式可能失效)
link.target = '_blank'; // 新标签页打开(避免阻塞当前页面)
// 触发点击下载
document.body.appendChild(link);
link.click();
document.body.removeChild(link); // 下载后移除标签
// 提示用户(H5 无法判断是否下载成功,仅提示触发)
uni.showToast({ title: '下载已触发,请在浏览器中确认', icon: 'none' });
}
场景 2:兼容两端的统一调用方案(条件编译)
通过 Uniapp 条件编译,让代码自动适配 H5 和小程序,无需手动判断环境:
/**
* 跨端下载文件(统一调用入口)
* @param {String} url - 下载地址
* @param {String} fileName - 自定义文件名
*/
crossDownload(url, fileName = '文件') {
// #ifdef H5
this.h5Download(url, fileName);
// #endif
// #ifdef MP
this.miniProgramDownload(url, fileName);
// #endif
}
模板中调用:
<template>
<button @click="crossDownload('https://example.***/file.pdf', '报表.pdf')">
下载文件(兼容 H5/小程序)
</button>
</template>
场景 3:H5 大文件下载(带进度条)
H5 中 a 标签无法监听进度,需通过 XMLHttpRequest 手动实现:
/**
* H5 大文件下载(带进度监听)
* @param {String} url - 下载地址
* @param {String} fileName - 自定义文件名
*/
h5LargeFileDownload(url, fileName = '文件') {
uni.showLoading({ title: '下载中...' });
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'blob'; // 响应类型为二进制流
// 监听进度
xhr.onprogress = (e) => {
if (e.length***putable) {
const progress = Math.round((e.loaded / e.total) * 100);
console.log(`下载进度:${progress}%`);
// 实时更新进度条(如:this.downloadProgress = progress)
}
};
// 下载完成
xhr.onload = function () {
uni.hideLoading();
if (xhr.status === 200) {
// 转换 blob 为下载链接
const blob = new Blob([xhr.response]);
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
// 释放 URL 对象,避免内存泄漏
URL.revokeObjectURL(url);
document.body.removeChild(link);
uni.showToast({ title: '下载成功', icon: 'su***ess' });
} else {
uni.showToast({ title: `下载失败:${xhr.status}`, icon: 'none' });
}
};
// 下载失败
xhr.onerror = function () {
uni.hideLoading();
uni.showToast({ title: '下载失败', icon: 'none' });
};
xhr.send();
}
四、避坑指南(关键注意点)
- 小程序合法域名:
- 必须配置下载地址的域名(https 协议),否则报错 downloadFile:fail url not in domain list。
- 本地开发时,可在微信开发者工具中勾选「不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书」(仅开发环境)。
- H5 跨域问题:
- 若下载地址与 H5 域名不同,后端必须配置 CORS 响应头,否则会出现 A***ess-Control-Allow-Origin 错误。
- 解决方案:后端配置 CORS 或使用 Nginx 代理转发下载请求。
- 文件命名兼容性:
- H5 中 a 标签的 download 属性对部分格式(如 .apk、.exe)可能失效,建议后端在响应头中设置文件名(Content-Disposition: attachment; filename="文件.pdf")。
- 临时文件清理:
- 小程序的 tempFilePath 会在退出后清理,重要文件务必通过 uni.saveFile 保存为永久路径。
- 可通过 uni.getSavedFileList() 查询已保存的文件,uni.removeSavedFile() 删除无用文件。
- H5 进度监听:
- 仅 XMLHttpRequest 支持进度监听,a 标签无法实现,大文件下载建议使用 XMLHttpRequest 方案。
五、总结
Uniapp 跨端下载的核心是「适配环境差异」:
- 小程序:利用 uni.downloadFile + uni.saveFile 实现完整的下载 - 保存 - 打开流程,支持进度监听和本地存储。
- H5:依赖浏览器原生能力(a 标签 /XMLHttpRequest),需处理跨域和进度监听的兼容性问题。
通过条件编译封装统一调用入口,可大幅减少冗余代码,实现「一套代码,两端运行」。实际开发中,需根据文件大小、格式和业务需求选择合适的方案,同时注意跨域、域名配置等细节,避免踩坑。