最近在 APP 中需要实现 文件导出功能,研究了一阵子,主要是文件保存到用户目录,并支持后续文件预览。为了避免自己以后再踩坑,这里快速做一个完整的记录。本文包含 manifest 权限配置、文件保存逻辑、文件预览打开 等核心步骤,适合有类似需求的同学参考。

在 uniapp 中,如果要在 IOS 和 Android 下将文件保存到用户本地目录,首先需要在 manifest.json 中配置文件读写权限。如果不配置,文件会默认保存到 Android 的包名路径或 IOS 的容器路径,用户无法方便地找到。
示例配置如下:
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>",
"<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>"
]
},
"apple": {
"plistcmds": [
"Set :NSFileProviderDomainUsageDescription 需要访问文件以保存用户数据",
"Set :UIFileSharingEnabled true",
"Set :LSSupportsOpeningDocumentsInPlace true"
]
}file://storage/emulated/0/ 为开头的路径,才能保存到真正的本地目录。_doc 目录。在实际开发中,最容易踩坑的就是 文件重复保存 问题。为了避免这种情况,建议先用 resolveLocalFileSystemURL 判断文件是否已存在:
plus.downloader.createDownload 下载到指定目录,再转为平台路径,然后再打开。下面贴一下完整的工具方法:
/**
* 保存并打开文件
* @param url 文件网络地址
*/
async function saveFile(url: string) {
const fileName = url.split("/").pop()?.split("?")[0];
console.log(url);
// #ifdef APP-PLUS
let realAbs = "";
const platform = uni.getSystemInfoSync().platform;
if (platform === "ios") {
realAbs = "_doc";
}
if (platform === "android") {
let granted = await permision.requestAndroidPermission(
"android.permission.WRITE_EXTERNAL_STORAGE"
);
if (granted === GrantedStatusEnum.GRANTED) {
realAbs = "file://storage/emulated/0/Download";
} else {
// 没有授权则保存到临时目录
realAbs = "_downloads";
notify.showNotify({
message: `未能授权文件权限,文件将临时保存`,
type: "danger",
});
}
}
const relPath = `${realAbs}/${fileName}`;
// 判断文件是否已存在
plus.io.resolveLocalFileSystemURL(
relPath,
async () => {
console.log("[saveFile] 文件已存在,直接打开");
if (platform === "android") {
await uni.showToast({
title: `文件已保存至: /Download/${fileName}`,
icon: "none",
});
}
openDocument(relPath);
},
() => {
console.log("[saveFile] 文件不存在,开始下载");
const task = plus.downloader.createDownload(
url,
{ filename: relPath },
async (d, status) => {
if (status === 200) {
if (platform === "android") {
await uni.showToast({
title: `文件已保存至: /Download/${fileName}`,
icon: "none",
});
}
const realAbs = plus.io.convertLocalFileSystemURL(d.filename);
openDocument(realAbs);
} else {
notify.showNotify({
message: `文件导出失败, 请稍后重试`,
type: "danger",
});
}
}
);
task.start();
}
);
// #endif
// #ifdef H5
location.href = url;
// #endif
}
function openDocument(absPath: string) {
plus.runtime.openFile(absPath, {}, (res) => {
if (res.code === -1) {
notify.showNotify({
message: "无法打开文件,请检查是否安装对应应用",
type: "danger",
});
}
});
}/Download 目录,用户可以直观地在系统下载文件夹找到导出的文件。_downloads 并未生效,猜测需要额外权限请求。暂时采取写入 _doc 的方案,虽然需要用户在预览后手动保存,但能保证功能基本可用。这次研究主要解决了 APP 文件导出与预览 的核心问题,过程中最关键的是权限和文件路径的处理。IOS 的部分还有待继续优化,等后续摸清楚具体的保存规则后会再更新。
如果你在开发中也遇到类似问题,可以先参考本文的实现思路,至少能保证大部分场景下功能可用。后续如果有更优解,我也会继续更新笔记。