Skip to content

文件系统与本地存储

预计阅读 15 分钟
版本信息
最后更新:2026-07-09 · 适用基础库:≥ 3.9.0(最低兼容 ≥ 2.30.0) · 微信开发者工具:稳定版 1.06+

微信小游戏提供完整的文件系统能力和本地存储 API,用于资源缓存、用户存档、日志存储等场景。本章讲解核心用法与最佳实践。


存储方案对比

API容量适用场景数据类型
wx.setStorageSync≤ 10MB用户设置、存档、token 等小数据字符串/对象(被序列化)
wx.getFileSystemManager受设备限制,通常 200MB+图片缓存、音频缓存、大文件Buffer / 二进制
云开发数据库按套餐跨设备同步的存档JSON 文档

wx.Storage — 同步键值存储

适合小量数据快速读写:

typescript
class GameStorage {
  /** 保存存档 */
  static save(key: string, data: unknown): void {
    try {
      wx.setStorageSync(key, JSON.stringify(data))
    } catch (err) {
      console.error('存档保存失败', err)
      // 存储空间不足时降级处理
      this.clearOldData()
    }
  }

  /** 读取存档 */
  static load<T>(key: string): T | null {
    try {
      const raw = wx.getStorageSync(key)
      return raw ? (JSON.parse(raw) as T) : null
    } catch {
      return null
    }
  }

  /** 存储使用情况 */
  static getInfo(): { currentSize: number; limitSize: number } {
    const info = wx.getStorageInfoSync()
    return {
      currentSize: info.currentSize, // KB
      limitSize: info.limitSize, // KB
    }
  }

  /** 清理过期数据 */
  private static clearOldData(): void {
    wx.getStorageInfo({
      success: (info) => {
        const keysToDelete = info.keys.slice(0, Math.floor(info.keys.length * 0.3))
        keysToDelete.forEach((key) => {
          if (!key.startsWith('perm_')) {
            wx.removeStorageSync(key)
          }
        })
      },
    })
  }
}

wx.getFileSystemManager — 文件系统

核心路径

typescript
const fs = wx.getFileSystemManager()

// 用户文件目录(持久化,可读写)
const userPath = wx.env.USER_DATA_PATH // 如: wxfile://usr/

// 临时文件目录(可能被系统清理)
// 通过 wx.getFileSystemManager() 操作的临时路径

写文件

typescript
// 写入用户存档
const saveGameData = (data: object): void => {
  const filePath = `${wx.env.USER_DATA_PATH}/save.json`
  fs.writeFile({
    filePath,
    data: JSON.stringify(data),
    encoding: 'utf8',
    success: () => console.log('存档已保存'),
    fail: (err) => console.error('存档失败', err),
  })
}

// 写入二进制缓存(如从 CDN 下载的图片)
const cacheImage = async (url: string, key: string): Promise<string> => {
  const cacheDir = `${wx.env.USER_DATA_PATH}/cache`
  const cachePath = `${cacheDir}/${key}.png`

  // 首次运行时创建缓存目录(mkdirSync 第二个参数为 recursive)
  try {
    fs.accessSync(cacheDir)
  } catch {
    fs.mkdirSync(cacheDir, true)
  }

  // 检查是否已缓存
  try {
    fs.accessSync(cachePath)
    return cachePath // 命中缓存
  } catch {
    // 未缓存,下载
  }

  return new Promise((resolve, reject) => {
    wx.downloadFile({
      url,
      success: (res) => {
        if (res.statusCode === 200) {
          // 移动到缓存目录
          fs.saveFile({
            tempFilePath: res.tempFilePath,
            filePath: cachePath,
            success: () => resolve(cachePath),
            fail: reject,
          })
        }
      },
      fail: reject,
    })
  })
}

读文件

typescript
// 读取用户存档
const loadGameData = <T = object>(): T | null => {
  const filePath = `${wx.env.USER_DATA_PATH}/save.json`
  try {
    const data = fs.readFileSync(filePath, 'utf8')
    return JSON.parse(data) as T
  } catch {
    return null
  }
}

目录管理与清理

typescript
const FSHelper = {
  /** 确保目录存在 */
  ensureDir(dirPath: string): void {
    try {
      fs.accessSync(dirPath)
    } catch {
      fs.mkdirSync(dirPath, true)
    }
  },

  /** 获取目录大小 */
  getDirSize(dirPath: string): number {
    const files = fs.readdirSync(dirPath)
    return files.reduce((total, file) => {
      try {
        const stat = fs.statSync(`${dirPath}/${file}`)
        return total + stat.size
      } catch {
        return total
      }
    }, 0)
  },

  /** 清理过期缓存(如超过 7 天的临时文件) */
  cleanExpiredCache(maxAgeMs: number = 7 * 24 * 3600 * 1000): number {
    const now = Date.now()
    const cacheDir = `${wx.env.USER_DATA_PATH}/cache`
    let freedBytes = 0

    try {
      const files = fs.readdirSync(cacheDir)
      files.forEach((file) => {
        const filePath = `${cacheDir}/${file}`
        try {
          const stat = fs.statSync(filePath)
          if (now - stat.lastModifiedTime > maxAgeMs) {
            freedBytes += stat.size
            fs.unlinkSync(filePath)
          }
        } catch {
          /* 跳过无法操作的文件 */
        }
      })
    } catch {
      /* 缓存目录可能不存在 */
    }

    return freedBytes
  },
}

实战:图片缓存层

typescript
class ImageCache {
  private static CACHE_DIR = `${wx.env.USER_DATA_PATH}/imgCache`
  private static MAX_CACHE_MB = 50

  /** 初始化缓存目录并清理超额 */
  static init(): void {
    FSHelper.ensureDir(this.CACHE_DIR)
    this.enforceLimit()
  }

  /** 获取图片(优先缓存) */
  static async get(url: string): Promise<string> {
    const cachePath = this.cachePath(url)

    // 检查缓存
    try {
      fs.accessSync(cachePath)
      return cachePath
    } catch {
      /* 未缓存,继续下载 */
    }

    // 下载并缓存
    return new Promise((resolve, reject) => {
      wx.downloadFile({
        url,
        success: (res) => {
          if (res.statusCode === 200) {
            fs.saveFile({
              tempFilePath: res.tempFilePath,
              filePath: cachePath,
              success: () => resolve(cachePath),
              fail: reject,
            })
          } else {
            reject(new Error(`HTTP ${res.statusCode}`))
          }
        },
        fail: reject,
      })
    })
  }

  private static cachePath(url: string): string {
    // 用 URL 生成唯一文件名
    const hash = url.split('').reduce((h, c) => ((h << 5) - h + c.charCodeAt(0)) | 0, 0)
    return `${this.CACHE_DIR}/${Math.abs(hash)}.cache`
  }

  /** 限制缓存大小,超过时清除最老的文件 */
  static enforceLimit(): void {
    const limitBytes = this.MAX_CACHE_MB * 1024 * 1024
    const files = fs
      .readdirSync(this.CACHE_DIR)
      .map((f) => `${this.CACHE_DIR}/${f}`)
      .map((p) => ({ path: p, ...fs.statSync(p) }))
      .sort((a, b) => a.lastModifiedTime - b.lastModifiedTime)

    let totalSize = files.reduce((s, f) => s + f.size, 0)

    while (totalSize > limitBytes && files.length > 0) {
      const oldest = files.shift()!
      totalSize -= oldest.size
      fs.unlinkSync(oldest.path)
    }
  }
}

文件与存储最佳实践
  1. 存档加密 — 敏感数据(如玩家进度)使用 AES 或至少简单混淆后存储。
  2. 定期备份 — 关键存档上传到云数据库或服务端做容灾备份。
  3. 缓存版本化 — 资源 URL 包含版本号,避免旧缓存影响新资源加载。
  4. 低频写入 — Storage/文件写入合并批处理,避免在游戏循环中操作。
  5. 错误兜底 — 所有文件操作必须 try-catch,存储满时优雅降级。

📝 课后练习

练习 1:存储选型

题目: 以下三种数据分别最适合用哪种存储方式?说明理由。

  1. 玩家最高分(< 1KB,需持久化)
  2. 关卡地图数据(每关 500KB,20 关)
  3. 玩家头像缓存(50KB,100+ 个头像)

参考答案:

  1. wx.setStorageSync — 数据极小、读写频繁、需要同步读取,wx.setStorageSync 最方便
  2. 文件系统(wx.getFileSystemManager — 数据量大,按需加载单独文件,配合分包管理
  3. 文件系统 + LRU 缓存 — 头像数量多但不需要全量保存(参见本章 ImageCache 示例),文件系统适合二进制数据存储
练习 2:存档安全

题目: 玩家存档被篡改(修改了金币数),如何检测和防范?

参考答案:

  1. 校验和 — 存档末尾附加 hash(数据 + 密钥),读取时校验
  2. 服务端备份 — 关键数据(如金币、付费道具)在服务端同步存储,以服务端为准
  3. 混淆 — 至少使用 base64 + 简单异或,增加破解成本
  4. 不信任前端 — 核心数据(付费状态、排名)永远以服务端为准
  5. 防回滚 — 存入版本号或时间戳,拒绝旧版本存档覆盖新版本

用心学习,持续实践