Uniapp 5+ API 文件操作:从基础到实战的深度解析与封装实践

在移动应用开发中,本地文件操作是一个绕不开的话题。无论是用户配置的持久化、离线数据的缓存,还是多媒体文件的处理,都离不开对文件系统的读写能力。对于使用 Uniapp 进行跨平台开发的开发者来说,5+ API 提供的 IO 模块是实现这些功能的关键桥梁。但很多开发者在使用过程中,常常会遇到各种问题:权限处理不当、路径理解混乱、异步操作处理复杂等。

这篇文章将从实战角度出发,深入剖析 Uniapp 中 5+ API 文件读写的完整流程。我不会简单地重复官方文档的内容,而是结合我多年在多个项目中积累的经验,分享一套经过验证的封装方案和最佳实践。无论你是需要实现简单的配置存储,还是复杂的文件管理系统,这篇文章都能为你提供清晰的思路和可复用的代码。

1. 理解 5+ API 文件系统的核心概念

在开始编写代码之前,我们需要先理解几个关键概念。很多开发者在使用 5+ API 时遇到的第一个困惑就是“路径”问题——为什么同样的代码在不同平台上表现不同?为什么有些路径可以访问,有些却不行?

1.1 文件系统类型与访问权限

5+ API 提供了多种文件系统类型,每种类型对应不同的存储位置和访问权限。理解这些差异是避免后续问题的关键。

// 常见的文件系统类型
const fsTypes = {
  PRIVATE_WWW: plus.io.PRIVATE_WWW,     // 应用资源目录,只读
  PRIVATE_DOC: plus.io.PRIVATE_DOC,     // 应用私有文档目录,可读写
  PUBLIC_DOCUMENTS: plus.io.PUBLIC_DOCUMENTS, // 公共文档目录
  PUBLIC_DOWNLOADS: plus.io.PUBLIC_DOWNLOADS  // 公共下载目录
};

注意:在 Android 10 及以上版本中,应用对公共存储区域的访问权限受到了更严格的限制。如果你需要访问外部存储(如 SD 卡),需要特别注意权限声明和运行时权限请求。

1.2 路径表示方式的差异

在 5+ API 中,路径有多种表示方式,理解它们的区别很重要:

路径类型示例说明适用场景
相对路径config.json相对于当前文件系统根目录应用私有文件操作
绝对路径/storage/emulated/0/Download/系统绝对路径需要精确控制位置时
URL 路径file:///storage/emulated/0/带协议的完整路径跨平台兼容性要求高时

在实际开发中,我推荐使用相对路径配合特定的文件系统类型,这样能获得更好的跨平台兼容性。比如,存储应用配置应该使用 PRIVATE_DOC,而用户下载的文件可以放在 PUBLIC_DOCUMENTS。

1.3 异步操作与 Promise 封装

5+ API 的文件操作大多是异步的,使用回调函数的方式。在现代 JavaScript 开发中,我们更习惯使用 async/await。下面是一个基础的封装模式:

// 将回调风格的 API 转换为 Promise
function requestFileSystemPromise(fsType) {
  return new Promise((resolve, reject) => {
    plus.io.requestFileSystem(fsType, (fs) => {
      resolve(fs);
    }, (error) => {
      reject(new Error(`请求文件系统失败: ${error.message}`));
    });
  });
}

// 使用示例
async function initFileSystem() {
  try {
    const fs = await requestFileSystemPromise(plus.io.PRIVATE_DOC);
    console.log('文件系统初始化成功');
    return fs;
  } catch (error) {
    console.error('文件系统初始化失败:', error);
    throw error;
  }
}

这种封装不仅让代码更清晰,还能更好地处理错误和异常情况。在实际项目中,我会建议将所有的文件操作都进行类似的 Promise 封装。

2. 文件读写的基础操作与封装

掌握了基本概念后,我们来深入看看具体的文件读写操作。很多开发者在初次接触时,会被各种 API 和方法搞得晕头转向。其实,核心操作只有几个,理解了它们之间的关系,就能举一反三。

2.1 读取文件的完整流程

读取文件不仅仅是调用一个 API 那么简单,它涉及到文件系统请求、文件获取、读取器创建等多个步骤。下面是我在实际项目中总结出的一个健壮的读取函数:

/**
 * 读取文本文件内容
 * @param {string} filePath - 文件路径(相对路径)
 * @param {string} encoding - 编码格式,默认 'utf-8'
 * @returns {Promise<string>} 文件内容
 */
async function readTextFile(filePath, encoding = 'utf-8') {
  return new Promise((resolve, reject) => {
    plus.io.requestFileSystem(plus.io.PRIVATE_DOC, (fs) => {
      // 获取文件入口
      fs.root.getFile(filePath, { create: false }, (fileEntry) => {
        // 获取文件对象
        fileEntry.file((file) => {
          const fileReader = new plus.io.FileReader();
          
          // 设置读取完成回调
          fileReader.onload = (event) => {
            resolve(event.target.result);
          };
          
          // 设置错误处理
          fileReader.onerror = (error) => {
            reject(new Error(`读取文件失败: ${error.message}`));
          };
          
          // 开始读取
          fileReader.readAsText(file, encoding);
        }, (error) => {
          reject(new Error(`获取文件对象失败: ${error.message}`));
        });
      }, (error) => {
        // 文件不存在时的处理
        if (error.code === FileError.NOT_FOUND_ERR) {
          resolve(''); // 返回空字符串而不是抛出错误
        } else {
          reject(new Error(`获取文件入口失败: ${error.message}`));
        }
      });
    }, (error) => {
      reject(new Error(`请求文件系统失败: ${error.message}`));
    });
  });
}

这个函数有几个值得注意的设计点:

  1. 完整的错误处理链:每个步骤都有独立的错误处理
  2. 文件不存在的优雅处理:返回空字符串而不是抛出异常
  3. Promise 封装:便于 async/await 使用

2.2 写入文件的进阶技巧

写入文件比读取更复杂,因为涉及到更多的边界情况处理。比如,文件不存在时是否创建、写入模式(覆盖还是追加)、大文件的分块写入等。

/**
 * 写入文本文件
 * @param {string} filePath - 文件路径
 * @param {string} content - 要写入的内容
 * @param {Object} options - 选项
 * @param {boolean} options.append - 是否追加模式,默认 false(覆盖)
 * @param {number} options.position - 写入位置(字节),append 为 true 时有效
 * @returns {Promise<void>}
 */
async function writeTextFile(filePath, content, options = {}) {
  const { append = false, position = 0 } = options;
  
  return new Promise((resolve, reject) => {
    plus.io.requestFileSystem(plus.io.PRIVATE_DOC, (fs) => {
      fs.root.getFile(filePath, { create: true }, (fileEntry) => {
        fileEntry.createWriter((writer) => {
          // 错误处理
          writer.onerror = (error) => {
            reject(new Error(`写入文件失败: ${error.message}`));
          };
          
          // 写入成功
          writer.onwrite = () => {
            resolve();
          };
          
          // 设置写入位置
          if (append) {
            writer.seek(position);
          } else {
            writer.seek(0); // 覆盖模式,从文件开始写入
            writer.truncate(0); // 清空文件内容
          }
          
          // 执行写入
          writer.write(content);
        }, (error) => {
          reject(new Error(`创建写入器失败: ${error.message}`));
        });
      }, (error) => {
        reject(new Error(`获取文件入口失败: ${error.message}`));
      });
    }, (error) => {
      reject(new Error(`请求文件系统失败: ${error.message}`));
    });
  });
}

提示:对于大文件的写入,建议使用分块写入的方式,避免内存占用过高。可以先将内容分割成多个小块,然后循环写入。

2.3 JSON 文件的特殊处理

在实际开发中,JSON 文件是最常用的配置文件格式。针对 JSON 文件,我们可以做一些专门的优化:

/**
 * 读取 JSON 文件
 * @param {string} filePath - JSON 文件路径
 * @param {any} defaultValue - 文件不存在或解析失败时的默认值
 * @returns {Promise<any>} 解析后的 JSON 对象
 */
async function readJSONFile(filePath, defaultValue = {}) {
  try {
    const content = await readTextFile(filePath);
    if (!content.trim()) {
      return defaultValue;
    }
    return JSON.parse(content);
  } catch (error) {
    console.warn(`读取或解析 JSON 文件失败: ${error.message}`);
    return defaultValue;
  }
}

/**
 * 写入 JSON 文件
 * @param {string} filePath - JSON 文件路径
 * @param {any} data - 要写入的数据
 * @param {number} space - JSON 格式化空格数
 * @returns {Promise<void>}
 */
async function writeJSONFile(filePath, data, space = 2) {
  const content = JSON.stringify(data, null, space);
  await writeTextFile(filePath, content);
}

// 使用示例:应用配置管理
class AppConfigManager {
  constructor(configPath = 'config/app.json') {
    this.configPath = configPath;
    this.config = null;
  }
  
  async load() {
    this.config = await readJSONFile(this.configPath, {
      version: '1.0.0',
      settings: {},
      lastUpdate: new Date().toISOString()
    });
    return this.config;
  }
  
  async save() {
    if (this.config) {
      this.config.lastUpdate = new Date().toISOString();
      await writeJSONFile(this.configPath, this.config);
    }
  }
  
  async update(key, value) {
    await this.load();
    if (typeof key === 'string') {
      this.config[key] = value;
    } else if (typeof key === 'object') {
      Object.assign(this.config, key);
    }
    await this.save();
  }
}

这种封装让 JSON 文件的操作变得非常简单直观,同时提供了良好的错误处理和默认值机制。

3. 文件与目录管理的高级操作

除了基本的读写操作,实际项目中经常需要对文件和目录进行更复杂的管理,比如创建目录、遍历文件、移动复制文件等。这些操作虽然不复杂,但细节很多,容易出错。

3.1 目录操作完整封装

目录操作是文件管理的基础。下面是一个完整的目录工具类,包含了常见的目录操作:

class DirectoryManager {
  /**
   * 创建目录(支持递归创建)
   * @param {string} dirPath - 目录路径
   * @returns {Promise<DirectoryEntry>} 创建的目录入口
   */
  static async createDirectory(dirPath) {
    return new Promise((resolve, reject) => {
      plus.io.resolveLocalFileSystemURL(
        `_doc/${dirPath}`,
        (entry) => {
          // 目录已存在
          resolve(entry);
        },
        async (error) => {
          // 目录不存在,需要创建
          if (error.code === FileError.NOT_FOUND_ERR) {
            try {
              // 递归创建父目录
              const parts = dirPath.split('/').filter(p => p);
              let currentPath = '_doc';
              
              for (const part of parts) {
                currentPath = `${currentPath}/${part}`;
                await this._createSingleDirectory(currentPath);
              }
              
              // 返回创建的目录
              plus.io.resolveLocalFileSystemURL(
                `_doc/${dirPath}`,
                resolve,
                reject
              );
            } catch (createError) {
              reject(createError);
            }
          } else {
            reject(new Error(`解析目录失败: ${error.message}`));
          }
        }
      );
    });
  }
  
  /**
   * 创建单个目录
   * @private
   */
  static _createSingleDirectory(fullPath) {
    return new Promise((resolve, reject) => {
      const parentPath = fullPath.substring(0, fullPath.lastIndexOf('/'));
      const dirName = fullPath.substring(fullPath.lastIndexOf('/') + 1);
      
      plus.io.resolveLocalFileSystemURL(parentPath, (parentEntry) => {
        parentEntry.getDirectory(dirName, { create: true }, resolve, reject);
      }, reject);
    });
  }
  
  /**
   * 列出目录内容
   * @param {string} dirPath - 目录路径
   * @returns {Promise<Array<{name: string, isFile: boolean, isDirectory: boolean}>>}
   */
  static async listDirectory(dirPath) {
    return new Promise((resolve, reject) => {
      plus.io.resolveLocalFileSystemURL(`_doc/${dirPath}`, (entry) => {
        if (!entry.isDirectory) {
          reject(new Error('路径不是目录'));
          return;
        }
        
        const directoryReader = entry.createReader();
        directoryReader.readEntries(
          (entries) => {
            const result = entries.map(entry => ({
              name: entry.name,
              isFile: entry.isFile,
              isDirectory: entry.isDirectory,
              fullPath: entry.fullPath,
              nativeURL: entry.toURL()
            }));
            resolve(result);
          },
          (error) => {
            reject(new Error(`读取目录失败: ${error.message}`));
          }
        );
      }, reject);
    });
  }
  
  /**
   * 删除目录(递归删除)
   * @param {string} dirPath - 目录路径
   * @returns {Promise<void>}
   */
  static async deleteDirectory(dirPath) {
    return new Promise((resolve, reject) => {
      plus.io.resolveLocalFileSystemURL(`_doc/${dirPath}`, (entry) => {
        if (entry.isDirectory) {
          entry.removeRecursively(resolve, reject);
        } else {
          reject(new Error('路径不是目录'));
        }
      }, reject);
    });
  }
}

这个目录管理类有几个实用的特性:

  1. 递归创建目录:像 a/b/c/d 这样的路径,即使父目录不存在也能自动创建
  2. 详细的目录列表:返回每个条目的完整信息,便于进一步处理
  3. 安全的删除操作:确保只删除目录,避免误删文件

3.2 文件移动、复制与重命名

文件的重命名、移动和复制是文件管理中的常见需求。虽然 5+ API 提供了相应的方法,但直接使用可能会遇到一些坑。

class FileOperations {
  /**
   * 移动或重命名文件
   * @param {string} srcPath - 源文件路径
   * @param {string} destPath - 目标路径
   * @param {string} newName - 新文件名(可选)
   * @returns {Promise<string>} 新文件的完整路径
   */
  static async moveFile(srcPath, destPath, newName = null) {
    return new Promise((resolve, reject) => {
      // 解析源文件
      plus.io.resolveLocalFileSystemURL(`_doc/${srcPath}`, (srcEntry) => {
        // 解析目标目录
        plus.io.resolveLocalFileSystemURL(`_doc/${destPath}`, (destEntry) => {
          if (!destEntry.isDirectory) {
            reject(new Error('目标路径不是目录'));
            return;
          }
          
          const finalName = newName || srcEntry.name;
          
          srcEntry.moveTo(destEntry, finalName, (newEntry) => {
            resolve(newEntry.fullPath);
          }, (error) => {
            // 处理常见错误
            if (error.code === FileError.PATH_EXISTS_ERR) {
              reject(new Error(`目标文件已存在: ${destPath}/${finalName}`));
            } else if (error.code === FileError.NO_MODIFICATION_ALLOWED_ERR) {
              reject(new Error('目标位置不允许修改'));
            } else {
              reject(new Error(`移动文件失败: ${error.message}`));
            }
          });
        }, (destError) => {
          // 目标目录不存在,尝试创建
          this._createParentDirectory(destPath)
            .then(() => this.moveFile(srcPath, destPath, newName))
            .then(resolve)
            .catch(reject);
        });
      }, (srcError) => {
        reject(new Error(`源文件不存在: ${srcPath}`));
      });
    });
  }
  
  /**
   * 复制文件
   * @param {string} srcPath - 源文件路径
   * @param {string} destPath - 目标路径
   * @param {string} newName - 新文件名(可选)
   * @returns {Promise<string>} 新文件的完整路径
   */
  static async copyFile(srcPath, destPath, newName = null) {
    return new Promise((resolve, reject) => {
      plus.io.resolveLocalFileSystemURL(`_doc/${srcPath}`, (srcEntry) => {
        plus.io.resolveLocalFileSystemURL(`_doc/${destPath}`, async (destEntry) => {
          if (!destEntry.isDirectory) {
            reject(new Error('目标路径不是目录'));
            return;
          }
          
          const finalName = newName || srcEntry.name;
          
          // 检查目标文件是否已存在
          try {
            const existing = await this._checkFileExists(destPath, finalName);
            if (existing) {
              // 生成唯一文件名
              const uniqueName = this._generateUniqueName(finalName);
              return this.copyFile(srcPath, destPath, uniqueName);
            }
          } catch (error) {
            // 检查过程中出错,继续执行
          }
          
          srcEntry.copyTo(destEntry, finalName, (newEntry) => {
            resolve(newEntry.fullPath);
          }, (error) => {
            reject(new Error(`复制文件失败: ${error.message}`));
          });
        }, async (destError) => {
          // 目标目录不存在,创建后重试
          try {
            await this._createParentDirectory(destPath);
            const result = await this.copyFile(srcPath, destPath, newName);
            resolve(result);
          } catch (createError) {
            reject(createError);
          }
        });
      }, (srcError) => {
        reject(new Error(`源文件不存在: ${srcPath}`));
      });
    });
  }
  
  /**
   * 检查文件是否存在
   * @private
   */
  static _checkFileExists(dirPath, fileName) {
    return new Promise((resolve) => {
      plus.io.resolveLocalFileSystemURL(
        `_doc/${dirPath}/${fileName}`,
        () => resolve(true),
        () => resolve(false)
      );
    });
  }
  
  /**
   * 生成唯一文件名
   * @private
   */
  static _generateUniqueName(originalName) {
    const dotIndex = originalName.lastIndexOf('.');
    let name = originalName;
    let ext = '';
    
    if (dotIndex > -1) {
      name = originalName.substring(0, dotIndex);
      ext = originalName.substring(dotIndex);
    }
    
    const timestamp = Date.now();
    const random = Math.floor(Math.random() * 1000);
    
    return `${name}_${timestamp}_${random}${ext}`;
  }
  
  /**
   * 创建父目录
   * @private
   */
  static async _createParentDirectory(filePath) {
    const lastSlash = filePath.lastIndexOf('/');
    if (lastSlash > -1) {
      const parentDir = filePath.substring(0, lastSlash);
      await DirectoryManager.createDirectory(parentDir);
    }
  }
}

这个文件操作类处理了很多边界情况:

  • 自动创建目标目录:如果目标目录不存在,会自动创建
  • 文件名冲突处理:复制时如果文件名冲突,会自动生成唯一文件名
  • 详细的错误信息:针对不同的错误类型提供具体的错误信息

3.3 文件信息获取与监控

有时候我们需要获取文件的详细信息,或者监控文件的变化。虽然 5+ API 没有直接提供文件监控功能,但我们可以通过其他方式实现类似的效果。

class FileInfoManager {
  /**
   * 获取文件信息
   * @param {string} filePath - 文件路径
   * @returns {Promise<FileMetadata>} 文件元数据
   */
  static async getFileInfo(filePath) {
    return new Promise((resolve, reject) => {
      plus.io.resolveLocalFileSystemURL(`_doc/${filePath}`, (entry) => {
        if (!entry.isFile) {
          reject(new Error('路径不是文件'));
          return;
        }
        
        entry.getMetadata((metadata) => {
          entry.file((file) => {
            resolve({
              name: entry.name,
              fullPath: entry.fullPath,
              nativeURL: entry.toURL(),
              size: file.size,
              type: file.type,
              lastModified: metadata.modificationTime,
              isFile: entry.isFile,
              isDirectory: entry.isDirectory
            });
          }, reject);
        }, reject);
      }, reject);
    });
  }
  
  /**
   * 计算目录大小(递归)
   * @param {string} dirPath - 目录路径
   * @returns {Promise<number>} 目录总大小(字节)
   */
  static async getDirectorySize(dirPath) {
    const entries = await DirectoryManager.listDirectory(dirPath);
    let totalSize = 0;
    
    for (const entry of entries) {
      if (entry.isFile) {
        const info = await this.getFileInfo(entry.fullPath.replace('_doc/', ''));
        totalSize += info.size;
      } else if (entry.isDirectory) {
        const subDirSize = await this.getDirectorySize(
          entry.fullPath.replace('_doc/', '')
        );
        totalSize += subDirSize;
      }
    }
    
    return totalSize;
  }
  
  /**
   * 格式化文件大小
   * @param {number} bytes - 字节数
   * @returns {string} 格式化后的字符串
   */
  static formatFileSize(bytes) {
    if (bytes === 0) return '0 B';
    
    const k = 1024;
    const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  }
  
  /**
   * 监控文件变化(轮询方式)
   * @param {string} filePath - 文件路径
   * @param {number} interval - 检查间隔(毫秒)
   * @param {Function} callback - 变化回调
   * @returns {Function} 停止监控的函数
   */
  static watchFile(filePath, interval, callback) {
    let lastModified = null;
    let lastSize = null;
    let isWatching = true;
    
    const checkFile = async () => {
      if (!isWatching) return;
      
      try {
        const info = await this.getFileInfo(filePath);
        const changed = 
          lastModified === null || 
          lastSize === null ||
          info.lastModified.getTime() !== lastModified.getTime() ||
          info.size !== lastSize;
        
        if (changed) {
          if (lastModified !== null) {
            callback({
              type: 'change',
              file: info,
              oldModified: lastModified,
              oldSize: lastSize
            });
          }
          
          lastModified = info.lastModified;
          lastSize = info.size;
        }
      } catch (error) {
        // 文件可能被删除
        if (lastModified !== null) {
          callback({
            type: 'delete',
            filePath: filePath
          });
          isWatching = false;
        }
      }
    };
    
    const intervalId = setInterval(checkFile, interval);
    checkFile(); // 立即执行一次
    
    return () => {
      isWatching = false;
      clearInterval(intervalId);
    };
  }
}

文件信息管理不仅提供了基本的元数据获取,还实现了目录大小计算和简单的文件监控功能。虽然轮询方式不如系统级的文件监控高效,但在很多场景下已经足够使用。

4. 实战应用:构建健壮的文件管理模块

有了前面的基础,我们现在可以构建一个完整的、生产可用的文件管理模块。这个模块将整合所有的功能,并提供统一的接口。

4.1 模块设计与架构

在设计文件管理模块时,我们需要考虑几个关键点:错误处理、性能优化、内存管理、以及用户体验。下面是一个完整的文件管理模块实现:

// file-manager.js
class FileManager {
  constructor(options = {}) {
    this.options = {
      defaultFsType: plus.io.PRIVATE_DOC,
      maxFileSize: 10 * 1024 * 1024, // 10MB
      retryCount: 3,
      retryDelay: 1000,
      ...options
    };
    
    this.cache = new Map(); // 简单的文件缓存
    this.watchers = new Map(); // 文件监控器
    this.queue = []; // 操作队列
    this.processing = false;
  }
  
  /**
   * 初始化文件管理器
   */
  async initialize() {
    try {
      // 测试文件系统是否可用
      await this.testFileSystem();
      console.log('文件管理器初始化成功');
      return true;
    } catch (error) {
      console.error('文件管理器初始化失败:', error);
      throw error;
    }
  }
  
  /**
   * 测试文件系统
   */
  async testFileSystem() {
    return new Promise((resolve, reject) => {
      plus.io.requestFileSystem(this.options.defaultFsType, (fs) => {
        // 创建测试文件
        fs.root.getFile(
          '.test_write',
          { create: true },
          (fileEntry) => {
            fileEntry.createWriter((writer) => {
              writer.onwrite = () => {
                // 删除测试文件
                fileEntry.remove(() => {
                  resolve(true);
                }, reject);
              };
              writer.onerror = reject;
              writer.write('test');
            }, reject);
          },
          reject
        );
      }, reject);
    });
  }
  
  /**
   * 带重试的文件操作
   */
  async withRetry(operation, operationName) {
    let lastError;
    
    for (let i = 0; i < this.options.retryCount; i++) {
      try {
        return await operation();
      } catch (error) {
        lastError = error;
        console.warn(`${operationName} 失败,第 ${i + 1} 次重试:`, error);
        
        if (i < this.options.retryCount - 1) {
          await this.delay(this.options.retryDelay);
        }
      }
    }
    
    throw new Error(`${operationName} 失败,已重试 ${this.options.retryCount} 次: ${lastError.message}`);
  }
  
  /**
   * 延迟函数
   */
  delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
  
  /**
   * 添加操作到队列
   */
  async enqueue(operation) {
    return new Promise((resolve, reject) => {
      this.queue.push({ operation, resolve, reject });
      this.processQueue();
    });
  }
  
  /**
   * 处理操作队列
   */
  async processQueue() {
    if (this.processing || this.queue.length === 0) {
      return;
    }
    
    this.processing = true;
    
    while (this.queue.length > 0) {
      const { operation, resolve, reject } = this.queue.shift();
      
      try {
        const result = await operation();
        resolve(result);
      } catch (error) {
        reject(error);
      }
      
      // 避免阻塞主线程
      await this.delay(0);
    }
    
    this.processing = false;
  }
  
  /**
   * 读取文件(带缓存)
   */
  async readFile(filePath, options = {}) {
    const {
      useCache = true,
      cacheKey = filePath,
      encoding = 'utf-8',
      validate = null
    } = options;
    
    // 检查缓存
    if (useCache && this.cache.has(cacheKey)) {
      const cached = this.cache.get(cacheKey);
      if (!validate || validate(cached)) {
        return cached;
      }
    }
    
    const operation = async () => {
      const content = await this.withRetry(
        () => readTextFile(filePath, encoding),
        `读取文件 ${filePath}`
      );
      
      if (useCache) {
        this.cache.set(cacheKey, content);
      }
      
      return content;
    };
    
    return this.enqueue(operation);
  }
  
  /**
   * 写入文件(带验证)
   */
  async writeFile(filePath, content, options = {}) {
    const {
      validate = null,
      backup = false,
      encoding = 'utf-8'
    } = options;
    
    // 内容验证
    if (validate && !validate(content)) {
      throw new Error('内容验证失败');
    }
    
    // 大小检查
    if (content.length > this.options.maxFileSize) {
      throw new Error(`文件大小超过限制: ${content.length} > ${this.options.maxFileSize}`);
    }
    
    const operation = async () => {
      // 备份原文件
      if (backup) {
        try {
          const backupPath = `${filePath}.backup_${Date.now()}`;
          await this.copyFile(filePath, backupPath);
        } catch (error) {
          // 备份失败不影响主操作
          console.warn('备份文件失败:', error);
        }
      }
      
      // 执行写入
      await this.withRetry(
        () => writeTextFile(filePath, content, { encoding }),
        `写入文件 ${filePath}`
      );
      
      // 清除缓存
      this.cache.delete(filePath);
      
      // 触发监控回调
      if (this.watchers.has(filePath)) {
        const callbacks = this.watchers.get(filePath);
        callbacks.forEach(callback => callback({
          type: 'write',
          filePath,
          timestamp: new Date()
        }));
      }
    };
    
    return this.enqueue(operation);
  }
  
  /**
   * 复制文件
   */
  async copyFile(srcPath, destPath, options = {}) {
    const { overwrite = false } = options;
    
    return this.enqueue(async () => {
      await this.withRetry(
        () => FileOperations.copyFile(srcPath, destPath),
        `复制文件 ${srcPath} -> ${destPath}`
      );
    });
  }
  
  /**
   * 监控文件变化
   */
  watch(filePath, callback, options = {}) {
    const { interval = 5000 } = options;
    
    if (!this.watchers.has(filePath)) {
      this.watchers.set(filePath, []);
    }
    
    const callbacks = this.watchers.get(filePath);
    callbacks.push(callback);
    
    // 如果这是第一个监控器,启动监控
    if (callbacks.length === 1) {
      const stopWatching = FileInfoManager.watchFile(
        filePath,
        interval,
        (event) => {
          callbacks.forEach(cb => cb(event));
        }
      );
      
      // 保存停止函数
      this.watchers.set(`_stop_${filePath}`, stopWatching);
    }
    
    // 返回取消监控的函数
    return () => {
      const index = callbacks.indexOf(callback);
      if (index > -1) {
        callbacks.splice(index, 1);
      }
      
      // 如果没有监控器了,停止监控
      if (callbacks.length === 0) {
        const stopWatching = this.watchers.get(`_stop_${filePath}`);
        if (stopWatching) {
          stopWatching();
          this.watchers.delete(`_stop_${filePath}`);
        }
        this.watchers.delete(filePath);
      }
    };
  }
  
  /**
   * 清理缓存
   */
  clearCache(pattern = null) {
    if (pattern) {
      const regex = new RegExp(pattern);
      for (const [key] of this.cache) {
        if (regex.test(key)) {
          this.cache.delete(key);
        }
      }
    } else {
      this.cache.clear();
    }
  }
  
  /**
   * 获取存储使用情况
   */
  async getStorageUsage() {
    try {
      const rootInfo = await FileInfoManager.getFileInfo('');
      const totalSize = await FileInfoManager.getDirectorySize('');
      
      return {
        total: totalSize,
        formatted: FileInfoManager.formatFileSize(totalSize),
        fileCount: await this.getFileCount(),
        directoryCount: await this.getDirectoryCount()
      };
    } catch (error) {
      console.error('获取存储使用情况失败:', error);
      return null;
    }
  }
  
  /**
   * 获取文件数量
   */
  async getFileCount(dirPath = '') {
    const entries = await DirectoryManager.listDirectory(dirPath);
    let count = 0;
    
    for (const entry of entries) {
      if (entry.isFile) {
        count++;
      } else if (entry.isDirectory) {
        const subCount = await this.getFileCount(
          entry.fullPath.replace('_doc/', '')
        );
        count += subCount;
      }
    }
    
    return count;
  }
  
  /**
   * 获取目录数量
   */
  async getDirectoryCount(dirPath = '') {
    const entries = await DirectoryManager.listDirectory(dirPath);
    let count = 0;
    
    for (const entry of entries) {
      if (entry.isDirectory) {
        count++; // 当前目录
        const subCount = await this.getDirectoryCount(
          entry.fullPath.replace('_doc/', '')
        );
        count += subCount; // 子目录
      }
    }
    
    return count;
  }
}

// 导出单例实例
export const fileManager = new FileManager();

这个文件管理模块提供了很多高级特性:

  1. 操作队列:确保文件操作按顺序执行,避免并发问题
  2. 重试机制:网络不稳定或系统繁忙时自动重试
  3. 缓存管理:减少重复读取,提高性能
  4. 文件监控:实时监控文件变化
  5. 存储统计:了解应用存储使用情况

4.2 在 Uniapp 项目中的集成使用

在实际的 Uniapp 项目中,我们可以这样使用这个文件管理模块:

// 在 main.js 或 app.vue 中初始化
import { fileManager } from './utils/file-manager';

export default {
  async onLaunch() {
    try {
      await fileManager.initialize();
      
      // 监听存储空间警告
      uni.onMemoryWarning(() => {
        console.log('内存警告,清理文件缓存');
        fileManager.clearCache();
      });
      
      // 定期清理旧备份文件
      this.setupCleanupTask();
      
    } catch (error) {
      console.error('应用启动失败 - 文件系统初始化错误:', error);
      uni.showModal({
        title: '初始化失败',
        content: '文件系统初始化失败,部分功能可能无法使用',
        showCancel: false
      });
    }
  },
  
  methods: {
    setupCleanupTask() {
      // 每天清理一次7天前的备份文件
      setInterval(async () => {
        try {
          const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
          await this.cleanupOldBackups(sevenDaysAgo);
        } catch (error) {
          console.error('清理备份文件失败:', error);
        }
      }, 24 * 60 * 60 * 1000);
    },
    
    async cleanupOldBackups(timestamp) {
      const entries = await DirectoryManager.listDirectory('');
      const backupFiles = entries.filter(entry => 
        entry.isFile && entry.name.includes('.backup_')
      );
      
      for (const file of backupFiles) {
        const info = await FileInfoManager.getFileInfo(file.fullPath);
        if (info.lastModified.getTime() < timestamp) {
          await fileManager.deleteFile(file.fullPath);
          console.log(`已删除旧备份文件: ${file.name}`);
        }
      }
    }
  }
};

在具体的页面或组件中使用:

<template>
  <view class="container">
    <view class="header">
      <text class="title">文件管理器</text>
      <text class="storage-info">
        已使用: {{ storageUsage.formatted || '计算中...' }}
      </text>
    </view>
    
    <view class="file-list">
      <view 
        v-for="item in fileList" 
        :key="item.fullPath"
        class="file-item"
        @click="handleFileClick(item)"
      >
        <text class="file-icon">{{ item.isDirectory ? '📁' : '📄' }}</text>
        <text class="file-name">{{ item.name }}</text>
        <text class="file-size" v-if="!item.isDirectory">
          {{ formatSize(item.size) }}
        </text>
        <text class="file-date">
          {{ formatDate(item.lastModified) }}
        </text>
      </view>
    </view>
    
    <view class="actions">
      <button @click="refreshFileList">刷新</button>
      <button @click="createNewFile">新建文件</button>
      <button @click="createNewFolder">新建文件夹</button>
    </view>
    
    <!-- 文件预览模态框 -->
    <modal v-if="previewFile" @close="previewFile = null">
      <view class="preview-content">
        <text class="preview-title">{{ previewFile.name }}</text>
        <textarea 
          v-if="isTextFile(previewFile.name)"
          v-model="fileContent"
          class="file-editor"
        />
        <image 
          v-else-if="isImageFile(previewFile.name)"
          :src="previewFile.nativeURL"
          mode="widthFix"
          class="preview-image"
        />
        <view v-else class="unsupported-file">
          不支持预览此文件类型
        </view>
        
        <view class="preview-actions">
          <button @click="saveFileContent" v-if="isTextFile(previewFile.name)">
            保存
          </button>
          <button @click="deleteCurrentFile">删除</button>
          <button @click="previewFile = null">关闭</button>
        </view>
      </view>
    </modal>
  </view>
</template>

<script>
import { fileManager } from '@/utils/file-manager';
import { DirectoryManager } from '@/utils/directory-manager';
import { FileInfoManager } from '@/utils/file-info-manager';

export default {
  data() {
    return {
      currentPath: '',
      fileList: [],
      storageUsage: {},
      previewFile: null,
      fileContent: '',
      isLoading: false
    };
  },
  
  async onLoad() {
    await this.loadFileList();
    await this.updateStorageUsage();
    
    // 监控当前目录变化
    this.unwatch = fileManager.watch(this.currentPath, (event) => {
      console.log('目录内容发生变化:', event);
      this.loadFileList();
    });
  },
  
  onUnload() {
    if (this.unwatch) {
      this.unwatch();
    }
  },
  
  methods: {
    async loadFileList() {
      this.isLoading = true;
      try {
        this.fileList = await DirectoryManager.listDirectory(this.currentPath);
        // 排序:目录在前,文件在后,按名称排序
        this.fileList.sort((a, b) => {
          if (a.isDirectory && !b.isDirectory) return -1;
          if (!a.isDirectory && b.isDirectory) return 1;
          return a.name.localeCompare(b.name);
        });
      } catch (error) {
        console.error('加载文件列表失败:', error);
        uni.showToast({
          title: '加载失败',
          icon: 'none'
        });
      } finally {
        this.isLoading = false;
      }
    },
    
    async updateStorageUsage() {
      this.storageUsage = await fileManager.getStorageUsage() || {};
    },
    
    formatSize(bytes) {
      return FileInfoManager.formatFileSize(bytes);
    },
    
    formatDate(date) {
      return new Date(date).toLocaleDateString();
    },
    
    isTextFile(filename) {
      const ext = filename.split('.').pop().toLowerCase();
      return ['txt', 'json', 'js', 'html', 'css', 'md'].includes(ext);
    },
    
    isImageFile(filename) {
      const ext = filename.split('.').pop().toLowerCase();
      return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(ext);
    },
    
    async handleFileClick(item) {
      if (item.isDirectory) {
        // 进入子目录
        this.currentPath = item.fullPath.replace('_doc/', '');
        await this.loadFileList();
      } else {
        // 预览文件
        this.previewFile = item;
        if (this.isTextFile(item.name)) {
          try {
            this.fileContent = await fileManager.readFile(
              item.fullPath.replace('_doc/', '')
            );
          } catch (error) {
            console.error('读取文件失败:', error);
            uni.showToast({
              title: '读取失败',
              icon: 'none'
            });
          }
        }
      }
    },
    
    async refreshFileList() {
      await this.loadFileList();
      await this.updateStorageUsage();
      uni.showToast({
        title: '已刷新',
        icon: 'success'
      });
    },
    
    async createNewFile() {
      uni.showModal({
        title: '新建文件',
        content: '请输入文件名:',
        editable: true,
        success: async (res) => {
          if (res.confirm && res.content) {
            const filename = res.content.trim();
            if (filename) {
              try {
                await fileManager.writeFile(
                  `${this.currentPath}/${filename}`,
                  ''
                );
                await this.loadFileList();
                uni.showToast({
                  title: '创建成功',
                  icon: 'success'
                });
              } catch (error) {
                console.error('创建文件失败:', error);
                uni.showToast({
                  title: '创建失败',
                  icon: 'none'
                });
              }
            }
          }
        }
      });
    },
    
    async createNewFolder() {
      uni.showModal({
        title: '新建文件夹',
        content: '请输入文件夹名:',
        editable: true,
        success: async (res) => {
          if (res.confirm && res.content) {
            const foldername = res.content.trim();
            if (foldername) {
              try {
                await DirectoryManager.createDirectory(
                  `${this.currentPath}/${foldername}`
                );
                await this.loadFileList();
                uni.showToast({
                  title: '创建成功',
                  icon: 'success'
                });
              } catch (error) {
                console.error('创建文件夹失败:', error);
                uni.showToast({
                  title: '创建失败',
                  icon: 'none'
                });
              }
            }
          }
        }
      });
    },
    
    async saveFileContent() {
      if (!this.previewFile || !this.isTextFile(this.previewFile.name)) {
        return;
      }
      
      try {
        await fileManager.writeFile(
          this.previewFile.fullPath.replace('_doc/', ''),
          this.fileContent,
          { backup: true }
        );
        
        uni.showToast({
          title: '保存成功',
          icon: 'success'
        });
        
        // 刷新文件列表
        await this.loadFileList();
        await this.updateStorageUsage();
        
      } catch (error) {
        console.error('保存文件失败:', error);
        uni.showToast({
          title: '保存失败',
          icon: 'none'
        });
      }
    },
    
    async deleteCurrentFile() {
      if (!this.previewFile) return;
      
      uni.showModal({
        title: '确认删除',
        content: `确定要删除 "${this.previewFile.name}" 吗?`,
        success: async (res) => {
          if (res.confirm) {
            try {
              if (this.previewFile.isDirectory) {
                await DirectoryManager.deleteDirectory(
                  this.previewFile.fullPath.replace('_doc/', '')
                );
              } else {
                await fileManager.deleteFile(
                  this.previewFile.fullPath.replace('_doc/', '')
                );
              }
              
              this.previewFile = null;
              await this.loadFileList();
              await this.updateStorageUsage();
              
              uni.showToast({
                title: '删除成功',
                icon: 'success'
              });
              
            } catch (error) {
              console.error('删除失败:', error);
              uni.showToast({
                title: '删除失败',
                icon: 'none'
              });
            }
          }
        }
      });
    }
  }
};
</script>

<style scoped>
.container {
  padding: 20rpx;
}

.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 30rpx;
  padding-bottom: 20rpx;
  border-bottom: 1rpx solid #eee;
}

.title {
  font-size: 36rpx;
  font-weight: bold;
}

.storage-info {
  font-size: 24rpx;
  color: #666;
}

.file-list {
  margin-bottom: 30rpx;
}

.file-item {
  display: flex;
  align-items: center;
  padding: 20rpx;
  border-bottom: 1rpx solid #f0f0f0;
}

.file-item:active {
  background-color: #f5f5f5;
}

.file-icon {
  margin-right: 20rpx;
  font-size: 40rpx;
}

.file-name {
  flex: 1;
  font-size: 28rpx;
}

.file-size {
  margin-right: 20rpx;
  font-size: 24rpx;
  color: #666;
}

.file-date {
  font-size: 24rpx;
  color: #999;
}

.actions {
  display: flex;
  gap: 20rpx;
}

button {
  flex: 1;
}

.preview-content {
  padding: 40rpx;
}

.preview-title {
  display: block;
  font-size: 32rpx;
  font-weight: bold;
  margin-bottom: 30rpx;
  text-align: center;
}

.file-editor {
  width: 100%;
  height: 600rpx;
  border: 1rpx solid #ddd;
  border-radius: 8rpx;
  padding: 20rpx;
  font-size: 28rpx;
  margin-bottom: 30rpx;
}

.preview-image {
  width: 100%;
  margin-bottom: 30rpx;
}

.unsupported-file {
  text-align: center;
  padding: 100rpx 0;
  color: #999;
  font-size: 28rpx;
}

.preview-actions {
  display: flex;
  gap: 20rpx;
}
</style>

这个示例展示了一个完整的文件管理器界面,包含了文件列表展示、文件预览、编辑、删除等基本功能。在实际项目中,你可以根据具体需求进行扩展,比如添加文件搜索、多选操作、批量处理等功能。

4.3 性能优化与最佳实践

在开发文件相关的功能时,性能是需要特别关注的点。以下是一些我在实际项目中总结的优化建议:

1. 批量操作优化

// 不好的做法:逐个文件操作
async function processFilesBad(files) {
  for (const file of files) {
    await processSingleFile(file); // 每次都要等待
  }
}

// 好的做法:批量处理
async function processFilesGood(files) {
  const batches = [];
  const batchSize = 10; // 每批处理10个文件
  
  for (let i = 0; i < files.length; i += batchSize) {
    const batch = files.slice(i, i + batchSize);
    batches.push(batch);
  }
  
  // 并行处理每个批次
  const results = await Promise.all(
    batches.map(batch => processBatch(batch))
  );
  
  return results.flat();
}

async function processBatch(files) {
  const promises = files.map(file => processSingleFile(file));
  return Promise.all(promises);
}

2. 缓存策略优化

class SmartCache {
  constructor(maxSize = 100, ttl = 5 * 60 * 1000) { // 默认5分钟
    this.cache = new Map();
    this.maxSize = maxSize;
    this.ttl = ttl;
  }
  
  set(key, value) {
    // 清理过期缓存
    this.cleanup();
    
    // 如果缓存已满,删除最旧的
    if (this.cache.size >= this.maxSize) {
      const oldestKey = this.cache.keys().next().value;
      this.cache.delete(oldestKey);
    }
    
    this.cache.set(key, {
      value,
      timestamp: Date.now()
    });
  }
  
  get(key) {
    const item = this.cache.get(key);
    if (!item) return null;
    
    // 检查是否过期
    if (Date.now() - item.timestamp > this.ttl) {
      this.cache.delete(key);
      return null;
    }
    
    return item.value;
  }
  
  cleanup() {
    const now = Date.now();
    for (const [key, item] of this.cache) {
      if (now - item.timestamp > this.ttl) {
        this.cache.delete(key);
      }
    }
  }
}

3. 内存管理

// 大文件分块读取
async function readLargeFile(filePath, chunkSize = 1024 * 1024) { // 1MB chunks
  const fileInfo = await FileInfoManager.getFileInfo(filePath);
  const totalSize = fileInfo.size;
  const chunks = [];
  
  for (let offset = 0; offset < totalSize; offset += chunkSize) {
    const chunk = await readFileChunk(filePath, offset, chunkSize);
    chunks.push(chunk);
    
    // 定期释放内存
    if (chunks.length % 10 === 0) {
      await new Promise(resolve => setTimeout(resolve, 0));
    }
  }
  
  return chunks;
}

async function readFileChunk(filePath, offset, size) {
  // 使用 FileReader 的 readAsArrayBuffer 方法读取指定范围
  return new Promise((resolve, reject) => {
    plus.io.resolveLocalFileSystemURL(`_doc/${filePath}`, (entry) => {
      entry.file((file) => {
        const fileReader = new plus.io.FileReader();
        const blob = file.slice(offset, offset + size);
        
        fileReader.onload = (event) => {
          resolve(event.target.result);
        };
        
        fileReader.onerror = reject;
        fileReader.readAsArrayBuffer(blob);
      }, reject);
    }, reject);
  });
}

4. 错误恢复机制

class FileOperationWithRecovery {
  constructor() {
    this.operationLog = [];
    this.maxLogSize = 100;
  }
  
  async safeOperation(operation, rollback) {
    const operationId = Date.now() + Math.random().toString(36).substr(2);
    const logEntry = {
      id: operationId,
      operation: operation.name || 'anonymous',
      timestamp: new Date(),
      state: 'pending'
    };
    
    this.operationLog.push(logEntry);
    if (this.operationLog.length > this.maxLogSize) {
      this.operationLog.shift();
    }
    
    try {
      const result = await operation();
      logEntry.state = 'success';
      logEntry.result = result;
      return result;
    } catch (error) {
      logEntry.state = 'failed';
      logEntry.error = error;
      
      // 尝试回滚
      try {
        if (rollback) {
          await rollback();
          logEntry.state = 'rolled_back';
        }
      } catch (rollbackError) {
        logEntry.state = 'rollback_failed';
        logEntry.rollbackError = rollbackError;
      }
      
      throw error;
    }
  }
  
  async recoverFromFailure() {
    const failedOperations = this.operationLog.filter(
      op => op.state === 'failed' || op.state === 'rollback_failed'
    );
    
    // 根据业务逻辑尝试恢复
    for (const op of failedOperations) {
      console.warn(`发现失败操作: ${op.operation}`, op.error);
      // 这里可以根据具体操作类型实现恢复逻辑
    }
  }
}

这些优化措施能够显著提升文件操作的性能和稳定性,特别是在处理大量文件或大文件时效果更加明显。

5. 跨平台兼容性处理与调试技巧

Uniapp 的跨平台特性意味着我们需要考虑不同平台的差异。虽然 5+ API 在大部分情况下表现一致,但仍有一些平台特定的问题需要注意。

5.1 平台差异处理

class PlatformAwareFileManager {
  constructor() {
    this.platform = uni.getSystemInfoSync().platform;
    this.osVersion = uni.getSystemInfoSync().system;
  }
  
  /**
   * 获取平台特定的文件系统根路径
   */
  getPlatformRootPath() {
    switch (this.platform) {
      case 'android':
        // Android 10+ 有存储限制
        if (this.isAndroid10OrAbove()) {
          return plus.io.PRIVATE_DOC; // 使用应用私有目录
        }
        return plus.io.PUBLIC_DOCUMENTS;
        
      case 'ios':
        // iOS 有更严格的沙盒限制
        return plus.io.PRIVATE_DOC;
        
      default:
        // 其他平台(如小程序)
        return plus.io.PRIVATE_DOC;
    }
  }
  
  /**
   * 检查是否是 Android 10 及以上
   */
  isAndroid10OrAbove() {
    if (this.platform !== 'android') return false;
    
    const versionMatch = this.osVersion.match(/Android (\d+)/);
    if (versionMatch && versionMatch[1]) {
      return parseInt(versionMatch[1]) >= 10;
    }
    
    return false;
  }
  
  /**
   * 平台特定的路径处理
   */
  normalizePath(path) {
    // 移除开头的斜杠
    if (path.startsWith('/')) {
      path = path.substring(1);
    }
    
    // 处理路径分隔符
    if (this.platform === 'windows') {
      path = path.replace(/\//g, '\\');
    }
    
    return path;
  }
  
  /**
   * 平台特定的权限检查
   */
  async checkPermissions() {
    const permissions = [];
    
    if (this.platform === 'android') {
      // Android 需要运行时权限
      const androidPermissions = [
        'android.permission.READ_EXTERNAL_STORAGE',
        'android.permission.WRITE_EXTERNAL_STORAGE'
      ];
      
      for (const perm of androidPermissions) {
        const granted = await this.checkAndroidPermission(perm);
        permissions.push({ permission: perm, granted });
      }
    }
    
    return permissions;
  }
  
  /**
   * 检查 Android 权限
   */
  checkAndroidPermission(permission) {
    return new Promise((resolve) => {
      plus.android.requestPermissions(
        [permission],
        (result) => {
          resolve(result.granted);
        },
        (error) => {
          console.error('检查权限失败:', error);
          resolve(false);
        }
      );
    });
  }
  
  /**
   * 平台特定的错误处理
   */
  handlePlatformError(error) {
    const errorMap = {
      'android': {
        'SECURITY_ERR': '权限不足,请检查应用权限设置',
        'NOT_FOUND_ERR': '文件或目录不存在',
        'NO_MODIFICATION_ALLOWED_ERR': '目标位置不允许修改'
      },
      'ios': {
        'NOT_FOUND_ERR': '文件或目录不存在',
        'NO_MODIFICATION_ALLOWED_ERR': '沙盒限制,无法修改'
      }
    };
    
    const platformErrors = errorMap[this.platform] || {};
    const userMessage = platformErrors[error.code] || error.message;
    
    return {
      originalError: error,
      platform: this.platform,
      userMessage,
      timestamp: new Date().toISOString()
    };
  }
}

5.2 调试与日志记录

在开发文件相关功能时,良好的调试和日志记录非常重要。下面是一个实用的调试工具:

class FileDebugger {
  constructor() {
    this.logs = [];
    this.maxLogs = 1000;
    this.enabled = process.env.NODE_ENV === 'development';
  }
  
  log(level, message, data = null) {
    if (!this.enabled) return;
    
    const logEntry = {
      level,
      message,
      data,
      timestamp: new Date().toISOString(),
      stack: new Error().stack
    };
    
    this.logs.push(logEntry);
    
    // 限制日志数量
    if (this.logs.length > this.maxLogs) {
      this.logs.shift();
    }
    
    // 控制台输出
    const consoleMethod = {
      'debug': console.debug,
      'info': console.info,
      'warn': console.warn,
      'error': console.error
    }[level] || console.log;
    
    consoleMethod(`[FileDebugger ${level.toUpperCase()}]`, message, data || '');
  }
  
  debug(message, data) {
    this.log('debug', message, data);
  }
  
  info(message, data) {
    this.log('info', message, data);
  }
  
  warn(message, data) {
    this.log('warn', message, data);
  }
  
  error(message, data) {
    this.log('error', message, data);
  }
  
  /**
   * 包装函数,自动记录输入输出和错误
   */
  wrapFunction(fn, name) {
    return async (...args) => {
      this.debug(`调用 ${name}`, { args });
      
      try {
        const startTime = Date.now();
        const result = await fn(...args);
        const duration = Date.now() - startTime;
        
        this.debug(`函数 ${name} 执行成功`, {
          duration: `${duration}ms`,
          result: typeof result === 'object' ? '[Object]' : result
        });
        
        return result;
      } catch (error) {
        this.error(`函数 ${name} 执行失败`, {
          error: error.message,
          stack: error.stack
        });
        throw error;
      }
    };
  }
  
  /**
   * 获取最近的日志
   */
  getRecentLogs(count = 50) {
    return this.logs.slice(-count);
  }
  
  /**
   * 导出日志
   */
  exportLogs() {
    return JSON.stringify(this.logs, null, 2);
  }
  
  /**
   * 保存日志到文件
   */
  async saveLogsToFile() {
    const logs = this.exportLogs();
    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
    const filename = `file-debug-logs-${timestamp}.json`;
    
    try {
      await fileManager.writeFile(`logs/${filename}`, logs);
      this.info('日志已保存到文件', { filename });
      return filename;
    } catch (error) {
      this.error('保存日志失败', { error: error.message });
      throw error;
    }
  }
  
  /**
   * 性能监控
   */
  startPerformanceMonitor() {
    const originalRequestFileSystem = plus.io.requestFileSystem;
    const originalGetFile = plus.io.requestFileSystem.prototype.getFile;
    
    // 监控 requestFileSystem
    plus.io.requestFileSystem = function(...args) {
      const startTime = Date.now();
      const result = originalRequestFileSystem.apply(this, args);
      
      if (typeof args[1] === 'function') {
        const originalCallback = args[1];
        args[1] = function(...callbackArgs) {
          const duration = Date.now() - startTime;
          FileDebugger.instance.debug('requestFileSystem 完成', {
            duration: `${duration}ms`,
            fsType: args[0]
          });
          return originalCallback.apply(this, callbackArgs);
        };
      }
      
      return result;
    };
    
    // 监控 getFile
    if (originalGetFile) {
      plus.io.requestFileSystem.prototype.getFile = function(...args) {
        const startTime = Date.now();
        const result = originalGetFile.apply(this, args);
        
        if (typeof args[1] === 'function') {
          const originalCallback = args[1];
          args[1] = function(...callbackArgs) {
            const duration = Date.now() - startTime;
            FileDebugger.instance.debug('getFile 完成', {
              duration: `${duration}ms`,
              path: args[0]
            });
            return originalCallback.apply(this, callbackArgs);
          };
        }
        
        return result;
      };
    }
    
    this.info('性能监控已启动');
  }
}

// 单例实例
FileDebugger.instance = new FileDebugger();

// 使用示例
const debuggedReadFile = FileDebugger.instance.wrapFunction(
  readTextFile,
  'readTextFile'
);

// 在开发环境中自动启动性能监控
if (process.env.NODE_ENV === 'development') {
  FileDebugger.instance.startPerformanceMonitor();
}

5.3 实际项目中的集成示例

最后,让我们看一个在实际项目中如何集成所有这些工具和最佳实践的完整示例:

// utils/file-system.js
import { FileDebugger } from './file-debugger';
import { PlatformAwareFileManager } from './platform-aware-file-manager';
import { fileManager } from './file-manager';

// 创建调试实例
const debugger = FileDebugger.instance;

// 创建平台感知的文件管理器
const platformManager = new PlatformAwareFileManager();

// 增强的文件操作函数(带调试和平台适配)
export const enhancedFileOperations = {
  async readFileWithDebug(filePath, options = {}) {
    const wrappedRead = debugger.wrapFunction(
      fileManager.readFile.bind(fileManager),
      'readFile'
    );
    
    try {
      // 平台特定的路径处理
      const normalizedPath = platformManager.normalizePath(filePath);
      
      // 检查权限(如果需要)
      if (platformManager.platform === 'android') {
        const permissions = await platformManager.checkPermissions();
        const hasStoragePermission = permissions.every(p => p.granted);
        
        if (!hasStoragePermission) {
          debugger.warn('存储权限不足,尝试使用私有目录');
          // 尝试使用私有目录
          const privatePath = `private/${normalizedPath}`;
          return await wrappedRead(privatePath, options);
        }
      }
      
      return await wrappedRead(normalizedPath, options);
      
    } catch (error) {
      const handledError = platformManager.handlePlatformError(error);
      debugger.error('读取文件失败', handledError);
      
      // 根据错误类型提供用户友好的提示
      if (error.code === 'NOT_FOUND_ERR') {
        if (options.createIfMissing) {
          debugger.info('文件不存在,创建新文件');
          await this.writeFileWithDebug(filePath, options.defaultContent || '', options);
          return options.defaultContent || '';
        }
      }
      
      throw handledError;
    }
  },
  
  async writeFileWithDebug(filePath, content, options = {}) {
    const wrappedWrite = debugger.wrapFunction(
      fileManager.writeFile.bind(fileManager),
      'writeFile'
    );
    
    try {
      const normalizedPath = platformManager.normalizePath(filePath);
      
      // 平台特定的预处理
      if (platformManager.platform === 'ios') {
        // iOS 对文件大小有限制
        const maxSize = 5 * 1024 * 1024; // 5MB
        if (content.length > maxSize) {
          debugger.warn('文件过大,尝试压缩或分块');
          // 这里可以添加压缩逻辑
        }
      }
      
      return await wrappedWrite(normalizedPath, content, options);
      
    } catch (error) {
      const handledError = platformManager.handlePlatformError(error);
      debugger.error('写入文件失败', handledError);
      
      // 尝试使用备用路径
      if (error.code === 'NO_MODIFICATION_ALLOWED_ERR') {
        debugger.info('尝试使用备用路径');
        const fallbackPath = `fallback/${filePath}`;
        return await this.writeFileWithDebug(fallbackPath, content, options);
      }
      
      throw handledError;
    }
  },
  
  // 批量操作(带进度回调)
  async batchProcess(files, processor, options = {}) {
    const {
      batchSize = 5,
      onProgress = null,
      onError = null
    } = options;
    
    const results = [];
    const errors = [];
    
    for (let i = 0; i < files.length; i += batchSize) {
      const batch = files.slice(i, i + batchSize);
      const batchPromises = batch.map(async (file, index) => {
        try {
          const result = await processor(file);
          return { success: true, data: result, file };
        } catch (error) {
          if (onError) {
            onError(error, file);
          }
          return { success: false, error, file };
        }
      });
      
      const batchResults = await Promise.all(batchPromises);
      results.push(...batchResults);
      
      // 更新进度
      if (onProgress) {
        const progress = Math.round((i + batch.length) / files.length * 100);
        onProgress(progress, batchResults);
      }
      
      // 避免阻塞主线程
      await new Promise(resolve => setTimeout(resolve, 0));
    }
    
    return {
      success: results.filter(r => r.success),
      failed: results.filter(r => !r.success),
      total: files.length,
      processed: results.length
    };
  },
  
  // 导出调试信息
  async exportDebugInfo() {
    const debugInfo = {
      platform: platformManager.platform,
      osVersion: platformManager.osVersion,
      rootPath: platformManager.getPlatformRootPath(),
      storageUsage: await fileManager.getStorageUsage(),
      recentLogs: debugger.getRecentLogs(100),
      timestamp: new Date().toISOString()
    };
    
    // 保存到文件
    const filename = `debug-info-${Date.now()}.json`;
    await this.writeFileWithDebug(
      `debug/${filename}`,
      JSON.stringify(debugInfo, null, 2)
    );
    
    return {
      filename,
      info: debugInfo
    };
  }
};

// 在应用启动时初始化
export async function initializeFileSystem() {
  debugger.info('初始化文件系统');
  
  try {
    // 初始化平台管理器
    await platformManager.initialize();
    
    // 初始化文件管理器
    await fileManager.initialize();
    
    // 检查存储空间
    const storageInfo = await fileManager.getStorageUsage();
    debugger.info('存储空间信息', storageInfo);
    
    // 如果存储空间不足,清理缓存
    if (storageInfo && storageInfo.total > 100 * 1024 * 1024) { // 超过100MB
      debugger.warn('存储空间较大,建议清理');
      fileManager.clearCache();
    }
    
    debugger.info('文件系统初始化完成');
    return true;
    
  } catch (error) {
    debugger.error('文件系统初始化失败', error);
    
    // 尝试降级方案
    try {
      debugger.info('尝试降级方案');
      // 这里可以实现降级逻辑,比如使用 IndexedDB 或 localStorage
      return await initializeFallbackFileSystem();
    } catch (fallbackError) {
      debugger.error('降级方案也失败', fallbackError);
      throw new Error('文件系统初始化完全失败');
    }
  }
}

// 降级方案
async function initializeFallbackFileSystem() {
  debugger.info('使用降级文件系统');
  
  // 检查是否支持 localStorage
  if (typeof localStorage !== 'undefined') {
    debugger.info('使用 localStorage 作为降级方案');
    // 实现基于 localStorage 的简单文件系统
    return true;
  }
  
  throw new Error('没有可用的降级方案');
}

// 错误边界组件
export const FileSystemErrorBoundary = {
  data() {
    return {
      hasError: false,
      error: null
    };
  },
  
  errorCaptured(err, vm, info) {
    debugger.error('文件系统错误边界捕获错误', {
      error: err.message,
      component: vm.$options.name,
      info
    });
    
    this.hasError = true;
    this.error = err;
    
    // 可以在这里上报错误
    this.reportError(err);
    
    return false; // 阻止错误继续向上传播
  },
  
  methods: {
    reportError(error) {
      // 错误上报逻辑
      console.error('文件系统错误:', error);
      
      // 可以保存错误信息到文件
      const errorLog = {
        type: 'file_system_error',
        message: error.message,
        stack: error.stack,
        timestamp: new Date().toISOString(),
        platform: platformManager.platform
      };
      
      // 异步保存,不阻塞主线程
      setTimeout(() => {
        enhancedFileOperations.writeFileWithDebug(
          'errors/file-system-errors.json',
          JSON.stringify(errorLog, null, 2),
          { appen

更多推荐