uniapp实战:5+API文件读写全流程解析(附完整代码)
Uniapp 5+API 文件读写:从基础到实战的深度解析与封装实践
在移动应用开发中,本地文件操作是一个绕不开的话题。无论是离线数据缓存、用户配置存储、日志记录,还是多媒体文件管理,都需要与设备的文件系统打交道。对于使用 Uniapp 进行跨平台开发的开发者来说,5+API 中的 IO 模块提供了强大而灵活的本地文件操作能力。但很多开发者在使用过程中,常常会遇到各种问题:权限问题、路径问题、异步回调处理、错误处理不完善等。
这篇文章将从实战角度出发,深入解析 Uniapp 中 5+API 文件读写的完整流程。我不会简单地重复官方文档的内容,而是结合我多年的开发经验,分享在实际项目中遇到的坑和解决方案,并提供一套经过实战检验的封装方案。无论你是刚刚接触 Uniapp 文件操作的新手,还是希望优化现有代码的老手,这篇文章都能给你带来新的启发。
1. 理解 5+API IO 模块的核心概念与架构
在开始编写代码之前,我们需要先理解 5+API IO 模块的基本架构。这个模块的设计遵循了 W3C 文件系统 API 的标准,但针对移动端做了优化和扩展。理解这个架构,能帮助我们在遇到问题时更快地定位原因。
1.1 文件系统类型与访问权限
5+API 提供了多种文件系统类型,每种类型对应不同的访问权限和存储位置:
// 常见的文件系统类型
plus.io.PRIVATE_WWW // 应用资源目录,只读
plus.io.PRIVATE_DOC // 应用私有文档目录,可读写
plus.io.PUBLIC_DOCUMENTS // 公共文档目录,可读写
plus.io.PUBLIC_DOWNLOADS // 公共下载目录,可读写
注意:从 Android 10(API 级别 29)开始,应用对公共存储区域的访问权限受到了更严格的限制。如果你的应用需要访问外部存储,需要特别注意权限适配问题。
在实际开发中,我通常根据数据的敏感性来选择存储位置:
| 数据类型 | 推荐存储位置 | 特点 | 适用场景 |
|---|---|---|---|
| 应用配置文件 | PRIVATE_DOC | 应用私有,用户不可见 | 用户设置、登录信息 |
| 用户生成的内容 | PUBLIC_DOCUMENTS | 用户可见,可备份 | 用户文档、图片 |
| 缓存数据 | PRIVATE_DOC 的子目录 | 可被系统清理 | 临时文件、图片缓存 |
| 下载的文件 | PUBLIC_DOWNLOADS | 用户可见,系统管理 | 用户下载的文件 |
1.2 核心对象关系图
理解以下几个核心对象的关系至关重要:
- FileSystem - 文件系统对象,通过
plus.io.requestFileSystem()获取 - DirectoryEntry - 目录条目,代表文件系统中的一个目录
- FileEntry - 文件条目,代表文件系统中的一个文件
- FileWriter - 文件写入器,用于向文件写入数据
- FileReader - 文件读取器,用于从文件读取数据
这些对象之间的关系可以用一个简单的流程来描述:首先获取文件系统,然后获取目录或文件条目,最后通过相应的读写器进行操作。
2. 文件读写基础:从零开始构建可靠的操作函数
很多教程直接给出封装好的代码,但我觉得理解每一步的原理更重要。下面我将拆解文件读写的每一个步骤,并解释为什么要这样处理。
2.1 异步操作与 Promise 封装
5+API 的文件操作都是异步的,使用回调函数。在现代 JavaScript 开发中,我们更倾向于使用 Promise 和 async/await,这样代码更清晰,错误处理更方便。
// 基础的文件系统请求函数
function requestFileSystem(fsType) {
return new Promise((resolve, reject) => {
plus.io.requestFileSystem(fsType, (fs) => {
resolve(fs);
}, (error) => {
reject(new Error(`请求文件系统失败: ${error.message}`));
});
});
}
// 获取文件条目的通用函数
function getFileEntry(fs, filePath, options = { create: false }) {
return new Promise((resolve, reject) => {
fs.root.getFile(filePath, options, (fileEntry) => {
resolve(fileEntry);
}, (error) => {
reject(new Error(`获取文件条目失败: ${error.message}`));
});
});
}
这里有几个关键点需要注意:
- 错误信息的处理:原生的错误对象可能信息不够详细,我通常会包装一下,加上上下文信息
- Promise 的 reject:一定要使用
Error对象,而不是字符串,这样能保留调用栈信息 - 选项参数:
create选项控制文件不存在时是否创建,根据业务需求灵活设置
2.2 完整的文件读取实现
读取文件看起来简单,但要做好错误处理和性能优化,需要考虑很多细节:
/**
* 读取文本文件内容
* @param {string} filePath - 文件路径(相对路径)
* @param {string} encoding - 编码格式,默认 'utf-8'
* @param {number} fsType - 文件系统类型,默认 plus.io.PRIVATE_DOC
* @returns {Promise<string>} 文件内容
*/
async function readTextFile(filePath, encoding = 'utf-8', fsType = plus.io.PRIVATE_DOC) {
try {
// 1. 请求文件系统
const fs = await requestFileSystem(fsType);
// 2. 获取文件条目
const fileEntry = await getFileEntry(fs, filePath, { create: false });
// 3. 获取文件对象
const file = await new Promise((resolve, reject) => {
fileEntry.file((fileObj) => {
resolve(fileObj);
}, (error) => {
reject(new Error(`获取文件对象失败: ${error.message}`));
});
});
// 4. 读取文件内容
const content = await new Promise((resolve, reject) => {
const fileReader = new plus.io.FileReader();
fileReader.onload = (event) => {
resolve(event.target.result);
};
fileReader.onerror = (error) => {
reject(new Error(`读取文件失败: ${error.message}`));
};
// 根据文件大小选择读取策略
if (file.size > 1024 * 1024) { // 大于1MB
console.warn(`文件较大(${(file.size / 1024 / 1024).toFixed(2)}MB),建议分片读取`);
}
fileReader.readAsText(file, encoding);
});
return content;
} catch (error) {
// 统一错误处理
console.error(`读取文件 ${filePath} 失败:`, error);
// 根据错误类型提供友好的用户提示
if (error.message.includes('未找到文件')) {
throw new Error(`文件 ${filePath} 不存在`);
} else if (error.message.includes('权限')) {
throw new Error('没有文件访问权限,请检查应用权限设置');
} else {
throw error;
}
}
}
这个实现考虑了:
- 大文件处理:超过1MB的文件会给出警告
- 详细的错误信息:区分不同类型的错误
- 完整的 Promise 链:使用 async/await 让代码更清晰
- 内存管理:及时清理不再使用的对象
2.3 文件写入的进阶技巧
写入文件比读取更复杂,因为涉及到写入位置、覆盖策略等问题:
/**
* 写入文本文件
* @param {string} filePath - 文件路径
* @param {string} content - 要写入的内容
* @param {Object} options - 写入选项
* @param {boolean} options.append - 是否追加,默认false(覆盖)
* @param {number} options.position - 写入位置(字节),append为true时有效
* @param {string} options.encoding - 编码格式,默认 'utf-8'
* @returns {Promise<void>}
*/
async function writeTextFile(filePath, content, options = {}) {
const {
append = false,
position = 0,
encoding = 'utf-8'
} = options;
try {
const fs = await requestFileSystem(plus.io.PRIVATE_DOC);
const fileEntry = await getFileEntry(fs, filePath, { create: true });
// 获取 FileWriter
const writer = await new Promise((resolve, reject) => {
fileEntry.createWriter((fileWriter) => {
resolve(fileWriter);
}, (error) => {
reject(new Error(`创建文件写入器失败: ${error.message}`));
});
});
return new Promise((resolve, reject) => {
// 设置写入位置
if (append) {
writer.seek(position);
} else {
writer.seek(0);
// 非追加模式,先截断文件
writer.truncate(0);
}
writer.onwrite = () => {
console.log(`文件 ${filePath} 写入成功`);
resolve();
};
writer.onerror = (error) => {
reject(new Error(`写入文件失败: ${error.message}`));
};
// 开始写入
writer.write(content);
});
} catch (error) {
console.error(`写入文件 ${filePath} 失败:`, error);
// 检查存储空间
if (error.message.includes('空间')) {
throw new Error('存储空间不足,请清理后重试');
}
throw error;
}
}
这里有几个实用的技巧:
- 文件截断:非追加模式时,先调用
truncate(0)清空文件 - 写入位置控制:通过
seek()方法精确控制写入位置 - 错误类型识别:识别存储空间不足等特定错误
3. 实战封装:构建企业级文件操作工具库
在实际项目中,我们需要的不仅仅是基础的读写功能,而是一套完整的文件操作解决方案。下面是我在多个项目中总结出来的工具库设计。
3.1 配置文件管理
很多应用需要管理配置文件,比如用户设置、应用状态等。我设计了一个配置管理器:
class ConfigManager {
constructor(configName = 'app-config.json') {
this.configPath = `configs/${configName}`;
this.cache = null;
this.lastModified = 0;
}
/**
* 读取配置
* @param {boolean} forceRefresh - 是否强制刷新缓存
* @returns {Promise<Object>}
*/
async getConfig(forceRefresh = false) {
// 使用缓存提高性能
if (!forceRefresh && this.cache && Date.now() - this.lastModified < 30000) {
return this.cache;
}
try {
const content = await readTextFile(this.configPath);
this.cache = JSON.parse(content);
this.lastModified = Date.now();
return this.cache;
} catch (error) {
// 文件不存在,返回默认配置
if (error.message.includes('不存在')) {
const defaultConfig = this.getDefaultConfig();
await this.saveConfig(defaultConfig);
return defaultConfig;
}
throw error;
}
}
/**
* 保存配置
* @param {Object} config - 配置对象
* @returns {Promise<void>}
*/
async saveConfig(config) {
// 验证配置格式
this.validateConfig(config);
// 合并现有配置(部分更新)
const currentConfig = await this.getConfig().catch(() => ({}));
const mergedConfig = { ...currentConfig, ...config };
// 序列化时美化格式
const content = JSON.stringify(mergedConfig, null, 2);
await writeTextFile(this.configPath, content);
// 更新缓存
this.cache = mergedConfig;
this.lastModified = Date.now();
// 触发配置变更事件
this.emit('configChanged', mergedConfig);
}
/**
* 获取配置项
* @param {string} key - 配置键,支持点符号(如 'user.theme')
* @param {any} defaultValue - 默认值
* @returns {Promise<any>}
*/
async getItem(key, defaultValue = null) {
const config = await this.getConfig();
// 支持嵌套属性访问
return key.split('.').reduce((obj, k) => {
return obj && obj[k] !== undefined ? obj[k] : defaultValue;
}, config);
}
/**
* 设置配置项
* @param {string} key - 配置键
* @param {any} value - 配置值
* @returns {Promise<void>}
*/
async setItem(key, value) {
const config = await this.getConfig();
// 支持嵌套属性设置
const keys = key.split('.');
const lastKey = keys.pop();
const target = keys.reduce((obj, k) => {
if (!obj[k] || typeof obj[k] !== 'object') {
obj[k] = {};
}
return obj[k];
}, config);
target[lastKey] = value;
await this.saveConfig(config);
}
// 其他辅助方法...
getDefaultConfig() {
return {
version: '1.0.0',
user: {
theme: 'light',
language: 'zh-CN'
},
app: {
firstLaunch: true,
lastVersion: ''
}
};
}
validateConfig(config) {
// 配置验证逻辑
if (typeof config !== 'object') {
throw new Error('配置必须是对象');
}
// 更多验证规则...
}
}
// 使用示例
const configManager = new ConfigManager();
// 读取配置
const config = await configManager.getConfig();
console.log('当前主题:', config.user.theme);
// 更新配置
await configManager.setItem('user.theme', 'dark');
await configManager.setItem('app.lastLaunch', new Date().toISOString());
这个配置管理器的特点:
- 智能缓存:30秒缓存,减少文件读取次数
- 自动创建:配置文件不存在时自动创建默认配置
- 部分更新:支持只更新部分配置项
- 嵌套属性:支持点符号访问嵌套属性
- 数据验证:写入前验证数据格式
3.2 文件分片上传与断点续传
对于大文件操作,我们需要更复杂的策略。下面是一个文件分片上传的实现:
class FileUploader {
constructor(options = {}) {
this.chunkSize = options.chunkSize || 1024 * 1024; // 1MB
this.maxRetries = options.maxRetries || 3;
this.concurrentUploads = options.concurrentUploads || 3;
this.progressCallbacks = [];
}
/**
* 分片上传文件
* @param {string} filePath - 本地文件路径
* @param {string} uploadUrl - 上传地址
* @param {Object} metadata - 文件元数据
* @returns {Promise<string>} 文件ID或URL
*/
async uploadFile(filePath, uploadUrl, metadata = {}) {
// 1. 获取文件信息
const fileInfo = await this.getFileInfo(filePath);
// 2. 初始化上传会话
const sessionId = await this.initUploadSession(uploadUrl, {
...metadata,
fileName: fileInfo.name,
fileSize: fileInfo.size,
totalChunks: Math.ceil(fileInfo.size / this.chunkSize)
});
// 3. 读取并上传分片
const chunks = await this.readFileChunks(filePath, fileInfo.size);
const results = await this.uploadChunks(chunks, uploadUrl, sessionId);
// 4. 完成上传
return await this.completeUpload(uploadUrl, sessionId, results);
}
/**
* 读取文件分片
*/
async readFileChunks(filePath, fileSize) {
const chunks = [];
const totalChunks = Math.ceil(fileSize / this.chunkSize);
for (let i = 0; i < totalChunks; i++) {
const start = i * this.chunkSize;
const end = Math.min(start + this.chunkSize, fileSize);
chunks.push({
index: i,
start,
end,
size: end - start
});
}
return chunks;
}
/**
* 上传分片(支持并发和重试)
*/
async uploadChunks(chunks, uploadUrl, sessionId) {
const results = new Array(chunks.length).fill(null);
const uploading = new Set();
// 进度跟踪
let uploadedCount = 0;
const updateProgress = () => {
uploadedCount++;
const progress = (uploadedCount / chunks.length) * 100;
this.progressCallbacks.forEach(callback => callback(progress));
};
// 并发控制
while (results.some(result => result === null)) {
// 找到未开始上传的分片
const availableSlots = this.concurrentUploads - uploading.size;
if (availableSlots <= 0) {
await this.sleep(100);
continue;
}
const nextChunkIndex = results.findIndex((result, index) =>
result === null && !uploading.has(index)
);
if (nextChunkIndex === -1) break;
uploading.add(nextChunkIndex);
this.uploadChunkWithRetry(
chunks[nextChunkIndex],
uploadUrl,
sessionId,
nextChunkIndex
).then(result => {
results[nextChunkIndex] = result;
uploading.delete(nextChunkIndex);
updateProgress();
}).catch(error => {
console.error(`分片 ${nextChunkIndex} 上传失败:`, error);
results[nextChunkIndex] = { error: error.message };
uploading.delete(nextChunkIndex);
});
}
// 等待所有上传完成
while (uploading.size > 0) {
await this.sleep(100);
}
return results;
}
/**
* 带重试机制的分片上传
*/
async uploadChunkWithRetry(chunk, uploadUrl, sessionId, chunkIndex) {
let lastError;
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
try {
// 读取分片数据
const chunkData = await this.readChunkData(chunk);
// 构建FormData
const formData = new FormData();
formData.append('sessionId', sessionId);
formData.append('chunkIndex', chunkIndex);
formData.append('chunkData', chunkData);
// 上传
const response = await uni.uploadFile({
url: uploadUrl,
filePath: chunkData,
name: 'chunkData',
formData: {
sessionId,
chunkIndex,
totalChunks: chunk.totalChunks
}
});
if (response.statusCode === 200) {
return JSON.parse(response.data);
} else {
throw new Error(`上传失败: ${response.statusCode}`);
}
} catch (error) {
lastError = error;
console.warn(`分片 ${chunkIndex} 第 ${attempt + 1} 次尝试失败:`, error);
if (attempt < this.maxRetries - 1) {
await this.sleep(1000 * Math.pow(2, attempt)); // 指数退避
}
}
}
throw lastError;
}
// 其他辅助方法...
async getFileInfo(filePath) {
// 获取文件信息的实现
}
async initUploadSession(uploadUrl, metadata) {
// 初始化上传会话的实现
}
async readChunkData(chunk) {
// 读取分片数据的实现
}
async completeUpload(uploadUrl, sessionId, results) {
// 完成上传的实现
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
onProgress(callback) {
this.progressCallbacks.push(callback);
}
}
// 使用示例
const uploader = new FileUploader({
chunkSize: 512 * 1024, // 512KB
concurrentUploads: 2
});
uploader.onProgress((progress) => {
console.log(`上传进度: ${progress.toFixed(1)}%`);
});
try {
const fileUrl = await uploader.uploadFile(
'/storage/emulated/0/Download/large-video.mp4',
'https://api.example.com/upload',
{
type: 'video',
description: '项目演示视频'
}
);
console.log('文件上传成功:', fileUrl);
} catch (error) {
console.error('上传失败:', error);
}
这个文件上传器的核心特性:
- 分片上传:支持大文件分片上传
- 断点续传:通过会话ID支持断点续传
- 并发控制:控制同时上传的分片数量
- 自动重试:失败时自动重试,使用指数退避策略
- 进度反馈:实时上传进度反馈
3.3 文件系统监控与同步
对于需要实时同步文件的应用,文件系统监控是必不可少的:
class FileSystemWatcher {
constructor(watchPath, options = {}) {
this.watchPath = watchPath;
this.interval = options.interval || 5000; // 检查间隔
this.recursive = options.recursive || false;
this.watchers = new Map(); // path -> { callback, lastModified }
this.timer = null;
this.fileCache = new Map();
}
/**
* 开始监控
*/
start() {
if (this.timer) {
console.warn('文件监控已启动');
return;
}
console.log(`开始监控目录: ${this.watchPath}`);
this.timer = setInterval(() => {
this.checkChanges();
}, this.interval);
}
/**
* 停止监控
*/
stop() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
console.log('停止文件监控');
}
}
/**
* 监控特定文件或目录
* @param {string} path - 相对路径
* @param {Function} callback - 变更回调
*/
watch(path, callback) {
const fullPath = this.normalizePath(path);
this.watchers.set(fullPath, {
callback,
lastModified: 0,
lastSize: 0
});
console.log(`开始监控: ${fullPath}`);
// 立即检查一次当前状态
this.checkFile(fullPath).then(fileInfo => {
if (fileInfo) {
this.watchers.get(fullPath).lastModified = fileInfo.lastModified;
this.watchers.get(fullPath).lastSize = fileInfo.size;
}
});
// 返回取消监控的函数
return () => {
this.unwatch(path);
};
}
/**
* 取消监控
*/
unwatch(path) {
const fullPath = this.normalizePath(path);
if (this.watchers.has(fullPath)) {
this.watchers.delete(fullPath);
console.log(`停止监控: ${fullPath}`);
}
}
/**
* 检查文件变更
*/
async checkChanges() {
const checkPromises = [];
for (const [path, watcher] of this.watchers) {
checkPromises.push(this.checkFile(path).then(fileInfo => {
if (!fileInfo) {
// 文件被删除
if (watcher.lastModified > 0) {
watcher.callback('deleted', null, path);
watcher.lastModified = 0;
watcher.lastSize = 0;
}
return;
}
// 检查变更
const changed =
fileInfo.lastModified !== watcher.lastModified ||
fileInfo.size !== watcher.lastSize;
if (changed) {
const changeType = watcher.lastModified === 0 ? 'created' : 'modified';
watcher.callback(changeType, fileInfo, path);
watcher.lastModified = fileInfo.lastModified;
watcher.lastSize = fileInfo.size;
}
}));
}
await Promise.allSettled(checkPromises);
}
/**
* 检查单个文件
*/
async checkFile(path) {
try {
const fs = await requestFileSystem(plus.io.PRIVATE_DOC);
const fileEntry = await getFileEntry(fs, path, { create: false });
const metadata = await new Promise((resolve, reject) => {
fileEntry.getMetadata(resolve, reject);
});
const file = await new Promise((resolve, reject) => {
fileEntry.file(resolve, reject);
});
return {
path,
lastModified: metadata.modificationTime.getTime(),
size: file.size,
type: file.type,
name: file.name
};
} catch (error) {
// 文件不存在
if (error.message.includes('未找到')) {
return null;
}
console.error(`检查文件 ${path} 失败:`, error);
return null;
}
}
/**
* 规范化路径
*/
normalizePath(path) {
// 移除开头的斜杠,确保相对路径
return path.replace(/^\//, '');
}
/**
* 批量监控目录下的所有文件
*/
async watchDirectory(dirPath, callback, fileFilter = null) {
const files = await this.listFiles(dirPath, this.recursive);
for (const file of files) {
if (fileFilter && !fileFilter(file)) {
continue;
}
this.watch(file.fullPath, (changeType, fileInfo) => {
callback(changeType, fileInfo, file.fullPath);
});
}
// 返回取消所有监控的函数
return () => {
for (const file of files) {
this.unwatch(file.fullPath);
}
};
}
/**
* 列出目录下的所有文件
*/
async listFiles(dirPath, recursive = false) {
const results = [];
try {
const fs = await requestFileSystem(plus.io.PRIVATE_DOC);
const dirEntry = await this.getDirectoryEntry(fs, dirPath);
const reader = dirEntry.createReader();
const entries = await new Promise((resolve, reject) => {
reader.readEntries(resolve, reject);
});
for (const entry of entries) {
if (entry.isFile) {
results.push({
name: entry.name,
fullPath: entry.fullPath,
isFile: true,
isDirectory: false
});
} else if (entry.isDirectory && recursive) {
// 递归处理子目录
const subFiles = await this.listFiles(entry.fullPath, true);
results.push(...subFiles);
}
}
} catch (error) {
console.error(`列出目录 ${dirPath} 失败:`, error);
}
return results;
}
async getDirectoryEntry(fs, dirPath) {
return new Promise((resolve, reject) => {
fs.root.getDirectory(dirPath, { create: false }, resolve, reject);
});
}
}
// 使用示例
const watcher = new FileSystemWatcher('documents', {
interval: 3000, // 每3秒检查一次
recursive: true // 递归监控子目录
});
// 开始监控
watcher.start();
// 监控特定文件
const unwatchConfig = watcher.watch('config/app-settings.json', (changeType, fileInfo) => {
console.log(`配置文件 ${changeType}:`, fileInfo);
if (changeType === 'modified') {
// 重新加载配置
this.reloadConfig();
}
});
// 监控整个目录
const unwatchAll = await watcher.watchDirectory('user-data', (changeType, fileInfo, path) => {
console.log(`文件变更 [${changeType}]: ${path}`);
// 同步到云端
if (changeType === 'modified' || changeType === 'created') {
this.syncToCloud(path);
}
});
// 停止监控(在组件销毁时调用)
// unwatchConfig();
// unwatchAll();
// watcher.stop();
文件系统监控器的设计考虑:
- 性能优化:使用缓存减少文件系统访问
- 变更检测:通过修改时间和文件大小检测变更
- 递归监控:支持监控整个目录树
- 精确控制:可以监控特定文件或整个目录
- 资源清理:提供取消监控的方法
4. 性能优化与最佳实践
在实际项目中,文件操作的性能直接影响用户体验。下面分享一些我在实践中总结的优化技巧。
4.1 文件操作性能优化
批量操作减少IO次数
class BatchFileProcessor {
constructor() {
this.operations = [];
this.batchSize = 10; // 每批处理10个操作
}
/**
* 添加读取操作
*/
addReadOperation(filePath, callback) {
this.operations.push({
type: 'read',
path: filePath,
callback
});
}
/**
* 添加写入操作
*/
addWriteOperation(filePath, content, callback) {
this.operations.push({
type: 'write',
path: filePath,
content,
callback
});
}
/**
* 执行批量操作
*/
async execute() {
const results = [];
const batches = this.chunkArray(this.operations, this.batchSize);
for (let i = 0; i < batches.length; i++) {
const batch = batches[i];
console.log(`处理第 ${i + 1}/${batches.length} 批,共 ${batch.length} 个操作`);
const batchResults = await Promise.allSettled(
batch.map(op => this.processOperation(op))
);
results.push(...batchResults);
// 批之间短暂延迟,避免阻塞UI
if (i < batches.length - 1) {
await this.sleep(50);
}
}
return results;
}
/**
* 处理单个操作
*/
async processOperation(operation) {
try {
let result;
switch (operation.type) {
case 'read':
result = await readTextFile(operation.path);
break;
case 'write':
await writeTextFile(operation.path, operation.content);
result = { success: true };
break;
default:
throw new Error(`未知操作类型: ${operation.type}`);
}
if (operation.callback) {
operation.callback(null, result);
}
return { status: 'fulfilled', value: result };
} catch (error) {
if (operation.callback) {
operation.callback(error, null);
}
return { status: 'rejected', reason: error };
}
}
/**
* 数组分块
*/
chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// 使用示例
const processor = new BatchFileProcessor();
// 添加多个读取操作
processor.addReadOperation('config/user.json', (error, content) => {
if (!error) {
console.log('读取用户配置成功');
}
});
processor.addReadOperation('data/cache.json', (error, content) => {
if (!error) {
console.log('读取缓存数据成功');
}
});
// 添加写入操作
processor.addWriteOperation('logs/app.log', '新的日志条目\n', (error) => {
if (!error) {
console.log('写入日志成功');
}
});
// 执行批量操作
const results = await processor.execute();
console.log(`批量操作完成,成功: ${results.filter(r => r.status === 'fulfilled').length}, 失败: ${results.filter(r => r.status === 'rejected').length}`);
内存使用优化
对于大文件操作,需要注意内存使用:
class StreamFileProcessor {
/**
* 流式读取大文件
*/
async readLargeFileStream(filePath, chunkSize = 64 * 1024, onChunk) {
const fs = await requestFileSystem(plus.io.PRIVATE_DOC);
const fileEntry = await getFileEntry(fs, filePath, { create: false });
const file = await new Promise((resolve, reject) => {
fileEntry.file(resolve, reject);
});
const totalSize = file.size;
let position = 0;
while (position < totalSize) {
const chunk = await this.readFileChunk(fileEntry, position, chunkSize);
if (onChunk) {
const shouldContinue = await onChunk(chunk, position, totalSize);
if (shouldContinue === false) {
break;
}
}
position += chunk.length;
// 定期释放内存
if (position % (10 * chunkSize) === 0) {
await this.sleep(0); // 让出控制权
}
}
return { totalSize, processed: position };
}
/**
* 读取文件块
*/
async readFileChunk(fileEntry, position, size) {
return new Promise((resolve, reject) => {
fileEntry.file((file) => {
const slice = file.slice(position, position + size);
const reader = new plus.io.FileReader();
reader.onload = (event) => {
resolve(event.target.result);
};
reader.onerror = reject;
reader.readAsArrayBuffer(slice);
}, reject);
});
}
/**
* 流式写入大文件
*/
async writeLargeFileStream(filePath, dataGenerator, chunkSize = 64 * 1024) {
const fs = await requestFileSystem(plus.io.PRIVATE_DOC);
const fileEntry = await getFileEntry(fs, filePath, { create: true });
const writer = await new Promise((resolve, reject) => {
fileEntry.createWriter(resolve, reject);
});
// 清空文件
writer.truncate(0);
writer.seek(0);
let position = 0;
let chunk;
while ((chunk = await dataGenerator()) !== null) {
// 确保chunk不超过指定大小
const chunks = this.splitChunk(chunk, chunkSize);
for (const subChunk of chunks) {
await new Promise((resolve, reject) => {
writer.onwrite = resolve;
writer.onerror = reject;
writer.write(subChunk);
});
position += subChunk.length;
// 定期保存进度
if (position % (10 * chunkSize) === 0) {
this.saveProgress(filePath, position);
}
}
// 释放内存
chunk = null;
await this.sleep(0);
}
return { totalSize: position };
}
splitChunk(data, maxSize) {
const chunks = [];
for (let i = 0; i < data.length; i += maxSize) {
chunks.push(data.slice(i, i + maxSize));
}
return chunks;
}
saveProgress(filePath, position) {
// 保存进度到临时文件
const progressPath = `${filePath}.progress`;
writeTextFile(progressPath, position.toString()).catch(() => {});
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// 使用示例:流式处理大文件
const processor = new StreamFileProcessor();
// 流式读取
await processor.readLargeFileStream('large-data.bin', 1024 * 1024, async (chunk, position, totalSize) => {
console.log(`处理进度: ${((position / totalSize) * 100).toFixed(1)}%`);
// 处理数据块
await processChunk(chunk);
// 返回true继续,false停止
return true;
});
// 流式写入
const dataGenerator = async () => {
// 生成数据块,返回null表示结束
if (noMoreData) {
return null;
}
return await generateNextChunk();
};
await processor.writeLargeFileStream('output.bin', dataGenerator);
4.2 错误处理与恢复
健壮的文件操作需要完善的错误处理和恢复机制:
class ResilientFileManager {
constructor() {
this.retryStrategies = {
'ENOENT': { // 文件不存在
maxRetries: 1,
action: 'create',
delay: 0
},
'EACCES': { // 权限错误
maxRetries: 2,
action: 'wait',
delay: 1000
},
'ENOSPC': { // 空间不足
maxRetries: 1,
action: 'cleanup',
delay: 0
},
'default': {
maxRetries: 3,
action: 'retry',
delay: 1000
}
};
this.errorStats = new Map();
}
/**
* 带重试的文件操作
*/
async withRetry(operation, context = {}) {
let lastError;
let retryCount = 0;
const strategy = this.getRetryStrategy(context.errorType);
while (retryCount <= strategy.maxRetries) {
try {
if (retryCount > 0) {
console.log(`第 ${retryCount} 次重试 ${context.operationName || '操作'}`);
// 执行重试前的操作
await this.beforeRetry(strategy.action, context);
// 延迟重试
if (strategy.delay > 0) {
await this.sleep(strategy.delay * Math.pow(2, retryCount - 1));
}
}
return await operation();
} catch (error) {
lastError = error;
retryCount++;
// 记录错误统计
this.recordError(error, context);
// 检查是否应该继续重试
if (!this.shouldRetry(error, retryCount, strategy)) {
break;
}
// 更新错误类型
context.errorType = this.getErrorType(error);
}
}
// 所有重试都失败
throw this.wrapError(lastError, retryCount, context);
}
/**
* 获取重试策略
*/
getRetryStrategy(errorType) {
return this.retryStrategies[errorType] || this.retryStrategies.default;
}
/**
* 重试前的操作
*/
async beforeRetry(action, context) {
switch (action) {
case 'create':
// 创建必要的目录
await this.ensureDirectory(context.filePath);
break;
case 'cleanup':
// 清理缓存
await this.cleanupCache();
break;
case 'wait':
// 等待系统资源
await this.waitForSystem();
break;
case 'retry':
// 普通重试,不做特殊操作
break;
}
}
/**
* 判断是否应该重试
*/
shouldRetry(error, retryCount, strategy) {
// 超过最大重试次数
if (retryCount > strategy.maxRetries) {
return false;
}
// 特定错误不重试
const errorMessage = error.message.toLowerCase();
if (errorMessage.includes('格式错误') ||
errorMessage.includes('不支持')) {
return false;
}
// 检查错误频率
const errorKey = this.getErrorKey(error);
const errorCount = this.errorStats.get(errorKey) || 0;
if (errorCount > 5) { // 相同错误出现太多次
return false;
}
return true;
}
/**
* 包装错误信息
*/
wrapError(error, retryCount, context) {
const wrappedError = new Error(
`${context.operationName || '操作'}失败(重试 ${retryCount - 1} 次): ${error.message}`
);
wrappedError.originalError = error;
wrappedError.retryCount = retryCount - 1;
wrappedError.context = context;
wrappedError.timestamp = new Date().toISOString();
// 保存错误日志
this.saveErrorLog(wrappedError);
return wrappedError;
}
/**
* 确保目录存在
*/
async ensureDirectory(filePath) {
const dirPath = filePath.substring(0, filePath.lastIndexOf('/'));
if (!dirPath) {
return;
}
try {
const fs = await requestFileSystem(plus.io.PRIVATE_DOC);
await new Promise((resolve, reject) => {
fs.root.getDirectory(dirPath, { create: true }, resolve, reject);
});
} catch (error) {
console.warn(`创建目录失败: ${dirPath}`, error);
}
}
/**
* 清理缓存
*/
async cleanupCache() {
const cacheDir = 'cache/';
const maxCacheSize = 100 * 1024 * 1024; // 100MB
try {
const files = await this.listFiles(cacheDir);
let totalSize = 0;
// 计算缓存大小
for (const file of files) {
totalSize += file.size || 0;
}
// 如果超过限制,清理最旧的文件
if (totalSize > maxCacheSize) {
files.sort((a, b) => a.lastModified - b.lastModified);
for (const file of files) {
if (totalSize <= maxCacheSize * 0.8) { // 清理到80%
break;
}
await this.deleteFile(file.fullPath);
totalSize -= file.size;
console.log(`清理缓存文件: ${file.name}`);
}
}
} catch (error) {
console.warn('清理缓存失败:', error);
}
}
/**
* 记录错误统计
*/
recordError(error, context) {
const errorKey = this.getErrorKey(error);
const count = this.errorStats.get(errorKey) || 0;
this.errorStats.set(errorKey, count + 1);
// 定期清理旧的统计
if (this.errorStats.size > 100) {
const keys = Array.from(this.errorStats.keys());
for (let i = 0; i < 20; i++) {
this.errorStats.delete(keys[i]);
}
}
}
getErrorKey(error) {
return `${error.code || 'NO_CODE'}:${error.message.substring(0, 50)}`;
}
getErrorType(error) {
const message = error.message.toLowerCase();
if (message.includes('no such file') || message.includes('未找到')) {
return 'ENOENT';
} else if (message.includes('permission') || message.includes('权限')) {
return 'EACCES';
} else if (message.includes('no space') || message.includes('空间')) {
return 'ENOSPC';
}
return 'default';
}
async saveErrorLog(error) {
const logEntry = {
timestamp: error.timestamp,
message: error.message,
originalMessage: error.originalError?.message,
retryCount: error.retryCount,
context: error.context,
stack: error.stack
};
const logPath = `logs/errors/${new Date().toISOString().split('T')[0]}.json`;
try {
// 读取现有日志
let logs = [];
try {
const content = await readTextFile(logPath);
logs = JSON.parse(content);
} catch (e) {
// 文件不存在,使用空数组
}
// 添加新日志
logs.push(logEntry);
// 限制日志数量
if (logs.length > 1000) {
logs = logs.slice(-500);
}
// 保存
await writeTextFile(logPath, JSON.stringify(logs, null, 2));
} catch (e) {
console.error('保存错误日志失败:', e);
}
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 其他辅助方法...
async listFiles(dirPath) {
// 实现文件列表获取
}
async deleteFile(filePath) {
// 实现文件删除
}
async waitForSystem() {
// 等待系统资源
await this.sleep(1000);
}
}
// 使用示例
const resilientManager = new ResilientFileManager();
// 使用重试机制进行文件操作
try {
const content = await resilientManager.withRetry(
async () => {
return await readTextFile('important-data.json');
},
{
operationName: '读取重要数据',
filePath: 'important-data.json'
}
);
console.log('数据读取成功:', content);
} catch (error) {
console.error('最终失败:', error);
// 根据错误类型采取不同措施
if (error.originalError?.message.includes('空间不足')) {
// 提示用户清理空间
uni.showModal({
title: '存储空间不足',
content: '请清理存储空间后重试',
showCancel: false
});
}
}
4.3 平台兼容性处理
不同平台(Android、iOS)的文件系统有差异,需要进行兼容性处理:
class PlatformFileAdapter {
constructor() {
this.platform = uni.getSystemInfoSync().platform;
this.isAndroid = this.platform === 'android';
this.isIOS = this.platform === 'ios';
this.platformSpecificPaths = {
android: {
publicDocuments: plus.io.PUBLIC_DOCUMENTS,
privateDocs: plus.io.PRIVATE_DOC,
downloads: plus.io.PUBLIC_DOWNLOADS,
externalStorage: 'file:///storage/emulated/0/'
},
ios: {
publicDocuments: plus.io.PUBLIC_DOCUMENTS,
privateDocs: plus.io.PRIVATE_DOC,
downloads: plus.io.PUBLIC_DOCUMENTS + '/Downloads/',
// iOS 没有直接的外部存储访问
externalStorage: null
}
};
}
/**
* 获取平台适配的文件路径
*/
getAdaptedPath(originalPath, options = {}) {
const {
accessLevel = 'private', // private, public, external
ensureExists = false
} = options;
let adaptedPath = originalPath;
// 处理路径分隔符
if (this.isAndroid) {
adaptedPath = adaptedPath.replace(/\\/g, '/');
} else if (this.isIOS) {
adaptedPath = adaptedPath.replace(/\\/g, '/');
}
// 根据访问级别调整路径
switch (accessLevel) {
case 'public':
adaptedPath = this.getPublicPath(adaptedPath);
break;
case 'external':
if (this.isAndroid) {
adaptedPath = this.getExternalPath(adaptedPath);
} else {
console.warn('iOS不支持外部存储访问,使用公共目录代替');
adaptedPath = this.getPublicPath(adaptedPath);
}
break;
case 'private':
default:
adaptedPath = this.getPrivatePath(adaptedPath);
break;
}
// 确保目录存在
if (ensureExists) {
this.ensureDirectoryExists(adaptedPath).catch(() => {
// 静默失败,在具体操作时再处理
});
}
return adaptedPath;
}
/**
* 获取私有路径
*/
getPrivatePath(relativePath) {
const basePath = this.platformSpecificPaths[this.platform].privateDocs;
return this.joinPath(basePath, relativePath);
}
/**
* 获取公共路径
*/
getPublicPath(relativePath) {
const basePath = this.platformSpecificPaths[this.platform].publicDocuments;
return this.joinPath(basePath, relativePath);
}
/**
* 获取外部存储路径(仅Android)
*/
getExternalPath(relativePath) {
if (!this.isAndroid) {
throw new Error('外部存储仅支持Android平台');
}
// 检查Android版本
const systemInfo = uni.getSystemInfoSync();
const androidVersion = parseInt(systemInfo.system.split(' ')[1]) || 0;
// Android 10+ 限制外部存储访问
if (androidVersion >= 10) {
console.warn('Android 10+ 限制外部存储访问,使用公共目录代替');
return this.getPublicPath(relativePath);
}
const basePath = this.platformSpecificPaths.android.externalStorage;
return this.joinPath(basePath, relativePath);
}
/**
* 连接路径
*/
joinPath(base, relative) {
if (!base) return relative;
if (!relative) return base;
const baseEndsWithSlash = base.endsWith('/');
const relativeStartsWithSlash = relative.startsWith('/');
if (baseEndsWithSlash && relativeStartsWithSlash) {
return base + relative.substring(1);
} else if (!baseEndsWithSlash && !relativeStartsWithSlash) {
return base + '/' + relative;
} else {
return base + relative;
}
}
/**
* 确保目录存在
*/
async ensureDirectoryExists(filePath) {
const dirPath = filePath.substring(0, filePath.lastIndexOf('/'));
if (!dirPath || dirPath === filePath) {
return; // 已经是根目录或没有目录部分
}
try {
const fs = await requestFileSystem(plus.io.PRIVATE_DOC);
// 递归创建目录
const parts = dirPath.split('/').filter(p => p);
let currentPath = '';
for (const part of parts) {
currentPath = currentPath ? `${currentPath}/${part}` : part;
await new Promise((resolve, reject) => {
fs.root.getDirectory(currentPath, { create: true }, resolve, reject);
});
}
} catch (error) {
console.warn(`创建目录失败: ${dirPath}`, error);
throw error;
}
}
/**
* 检查文件访问权限
*/
async checkPermission(filePath, operation = 'read') {
if (this.isIOS) {
// iOS 权限检查
return this.checkIOSPermission(filePath, operation);
} else if (this.isAndroid) {
// Android 权限检查
return this.checkAndroidPermission(filePath, operation);
}
return true; // 其他平台默认允许
}
/**
* iOS 权限检查
*/
async checkIOSPermission(filePath, operation) {
// iOS 的文件权限主要受沙盒限制
// 检查是否在沙盒允许的目录内
const allowedPaths = [
this.getPrivatePath(''),
this.getPublicPath('')
];
const isAllowed = allowedPaths.some(allowedPath =>
filePath.startsWith(allowedPath)
);
if (!isAllowed) {
throw new Error(`iOS不允许访问路径: ${filePath}`);
}
return true;
}
/**
* Android 权限检查
*/
async checkAndroidPermission(filePath, operation) {
const systemInfo = uni.getSystemInfoSync();
const androidVersion = parseInt(systemInfo.system.split(' ')[1]) || 0;
// 检查是否在外部存储
const isExternal = filePath.startsWith('file:///storage/');
if (isExternal && androidVersion >= 11) {
// Android 11+ 需要特殊权限
const hasPermission = await this.requestAndroidStoragePermission();
if (!hasPermission) {
throw new Error('需要存储权限才能访问外部文件');
}
}
return true;
}
/**
* 请求Android存储权限
*/
async requestAndroidStoragePermission() {
return new Promise((resolve) => {
// 使用5+API的权限请求
plus.android.requestPermissions(
['android.permission.READ_EXTERNAL_STORAGE', 'android.permission.WRITE_EXTERNAL_STORAGE'],
(result) => {
const granted = result.granted && result.granted.length === 2;
resolve(granted);
},
(error) => {
console.error('请求权限失败:', error);
resolve(false);
}
);
});
}
/**
* 获取平台特定的文件操作选项
*/
getPlatformOptions(options = {}) {
const platformOptions = { ...options };
if (this.isIOS) {
// iOS 特定选项
platformOptions.encoding = options.encoding || 'utf-8';
platformOptions.noBackup = options.noBackup !== false; // 默认不备份到iCloud
if (platformOptions.noBackup) {
// 设置文件不备份到iCloud
this.setFileNoBackupFlag(options.filePath).catch(() => {
// 静默失败
});
}
} else if (this.isAndroid) {
// Android 特定选项
platformOptions.encoding = options.encoding || 'utf-8';
// Android 7+ 需要FileProvider
const systemInfo = uni.getSystemInfoSync();
const androidVersion = parseInt(systemInfo.system.split(' ')[1]) || 0;
if (androidVersion >= 7 && options.shareable) {
platformOptions.useFileProvider = true;
}
}
return platformOptions;
}
/**
* 设置文件不备份到iCloud(iOS)
*/
async setFileNoBackupFlag(filePath) {
if (!this.isIOS) {
return;
}
try {
// 使用5+API设置文件属性
plus.io.resolveLocalFileSystemURL(filePath, (entry) => {
// iOS特定的属性设置
// 实际实现可能需要调用原生插件
console.log('设置文件不备份标志:', filePath);
}, (error) => {
console.warn('设置文件属性失败:', error);
});
} catch (error) {
console.warn('设置iCloud备份标志失败:', error);
}
}
/**
* 获取平台推荐的文件大小限制
*/
getRecommendedSizeLimit(operationType) {
const limits = {
android: {
read: 100 * 1024 * 1024, // 100MB
write: 50 * 1024 * 1024, // 50MB
memory: 256 * 1024 * 1024 // 256MB
},
ios: {
read: 50 * 1024 * 1024, // 50MB
write: 25 * 1024 * 1024, // 25MB
memory: 128 * 1024 * 1024 // 128MB
}
};
const platformLimits = this.isAndroid ? limits.android : limits.ios;
return platformLimits[operationType] || platformLimits.memory;
}
/**
* 检查文件大小是否安全
*/
isFileSizeSafe(fileSize, operationType) {
const limit = this.getRecommendedSizeLimit(operationType);
return fileSize <= limit;
}
}
// 使用示例
const fileAdapter = new PlatformFileAdapter();
// 获取适配后的路径
const privateConfigPath = fileAdapter.getAdaptedPath('config/settings.json', {
accessLevel: 'private',
ensureExists: true
});
const publicImagePath = fileAdapter.getAdaptedPath('images/avatar.jpg', {
accessLevel: 'public'
});
// 检查权限
try {
const hasPermission = await fileAdapter.checkPermission(publicImagePath, 'write');
if (hasPermission) {
// 执行文件操作
const options = fileAdapter.getPlatformOptions({
encoding: 'utf-8',
shareable: true
});
// 检查文件大小
const fileInfo = await getFileInfo(publicImagePath);
if (fileAdapter.isFileSizeSafe(fileInfo.size, 'write')) {
await writeTextFile(publicImagePath, imageData, options);
} else {
console.warn('文件过大,建议分片处理');
}
}
} catch (error) {
console.error('文件操作失败:', error);
// 根据平台提供不同的错误处理
if (fileAdapter.isIOS && error.message.includes('iCloud')) {
// 处理iCloud相关错误
} else if (fileAdapter.isAndroid && error.message.includes('权限')) {
// 处理权限错误
}
}
这个平台适配器的主要功能:
- 路径适配:根据不同平台调整文件路径
- 权限管理:检查和处理平台特定的权限要求
- 大小限制:提供平台推荐的文件大小限制
- 特殊处理:处理iCloud备份、FileProvider等平台特定需求
- 错误处理:提供平台特定的错误处理建议
通过这样的平台适配,可以大大减少跨平台开发中的兼容性问题,让文件操作代码更加健壮和可靠。
更多推荐

所有评论(0)