uniApp App 端日志本地存储方案:实现可靠的日志记录功能

在移动应用开发过程中,日志记录是排查问题、分析用户行为的重要手段。对于 UniApp 开发的 App 来说,实现日志本地存储并在需要时导出,能极大地方便问题定位。本文将介绍如何在 UniApp 的 App 端实现日志的本地文件存储功能。

功能需求分析

一个完善的本地日志存储方案应具备以下功能:

  • 支持写入不同级别日志(info、warn、error 等)
  • 自动记录日志时间和级别
  • 按日期分割日志文件,避免单个文件过大
  • 支持设置日志文件最大保存天数
  • 提供日志文件清理功能
  • 确保在 App 重启后日志不会丢失

实现方案

下面是一个完整的日志工具类实现,基于 UniApp 的文件系统 API:

// logger.js
const LOG_LEVELS = {
  DEBUG: 'DEBUG',
  INFO: 'INFO',
  WARN: 'WARN',
  ERROR: 'ERROR'
}

class Logger {
  constructor() {
    this.maxLogDays = 7 // 默认保存7天日志
    this.logDir = 'logs' // 日志目录
    this.init()
  }

  async init() {
    // 确保日志目录存在
    try {
      const dirInfo = await this.getDirInfo(this.logDir)
      if (!dirInfo) {
        await this.createDir(this.logDir)
      }
    } catch (e) {
      console.error('初始化日志目录失败:', e)
    }
    
    // 启动时清理过期日志
    this.cleanOldLogs()
  }

  // 设置日志最大保存天数
  setMaxLogDays(days) {
    if (days > 0) {
      this.maxLogDays = days
    }
  }

  // 获取当前日期字符串 (格式: YYYY-MM-DD)
  getCurrentDateStr() {
    const date = new Date()
    return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`
  }

  // 获取当前时间字符串
  getCurrentTimeStr() {
    const date = new Date()
    return `${this.getCurrentDateStr()} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')}.${date.getMilliseconds().toString().padStart(3, '0')}`
  }

  // 获取日志文件名
  getLogFileName() {
    return `app_${this.getCurrentDateStr()}.log`
  }

  // 写入日志
  async log(level, message, data = null) {
    try {
      const time = this.getCurrentTimeStr()
      let logContent = `[${time}] [${level}] ${message}`
      
      if (data) {
        logContent += ` | ${typeof data === 'object' ? JSON.stringify(data) : data}`
      }
      
      logContent += '\n'

      const fileName = this.getLogFileName()
      const filePath = `${this.logDir}/${fileName}`
      
      // 检查文件是否存在
      const fileInfo = await this.getFileInfo(filePath)
      
      if (fileInfo) {
        // 文件存在,追加内容
        await this.appendFile(filePath, logContent)
      } else {
        // 文件不存在,创建新文件
        await this.writeFile(filePath, logContent)
      }
    } catch (e) {
      console.error('写入日志失败:', e)
    }
  }

  // 快捷方法
  debug(message, data) {
    this.log(LOG_LEVELS.DEBUG, message, data)
  }

  info(message, data) {
    this.log(LOG_LEVELS.INFO, message, data)
  }

  warn(message, data) {
    this.log(LOG_LEVELS.WARN, message, data)
  }

  error(message, data) {
    this.log(LOG_LEVELS.ERROR, message, data)
  }

  // 清理过期日志
  async cleanOldLogs() {
    try {
      const now = new Date()
      const threshold = now.setDate(now.getDate() - this.maxLogDays)
      
      const dirInfo = await this.getDirInfo(this.logDir)
      if (!dirInfo) return
      
      const files = await this.readDir(this.logDir)
      
      for (const file of files) {
        const fileName = file.name
        // 解析文件名中的日期
        const dateStr = fileName.match(/app_(\d{4}-\d{2}-\d{2})\.log/)?.[1]
        if (dateStr) {
          const fileDate = new Date(dateStr)
          if (fileDate.getTime() < threshold) {
            // 文件日期早于阈值,删除
            await this.deleteFile(`${this.logDir}/${fileName}`)
          }
        }
      }
    } catch (e) {
      console.error('清理日志失败:', e)
    }
  }

  // 手动清理所有日志
  async clearAllLogs() {
    try {
      const files = await this.readDir(this.logDir)
      for (const file of files) {
        await this.deleteFile(`${this.logDir}/${file.name}`)
      }
    } catch (e) {
      console.error('清除所有日志失败:', e)
    }
  }

  // 获取所有日志文件列表
  async getLogFiles() {
    try {
      const files = await this.readDir(this.logDir)
      return files.filter(file => file.name.endsWith('.log'))
    } catch (e) {
      console.error('获取日志文件列表失败:', e)
      return []
    }
  }

  // 读取日志文件内容
  async readLogFile(fileName) {
    try {
      const filePath = `${this.logDir}/${fileName}`
      const content = await this.readFile(filePath)
      return content
    } catch (e) {
      console.error('读取日志文件失败:', e)
      return null
    }
  }

  // ========== 文件系统操作封装 ==========
  
  // 获取目录信息
  async getDirInfo(dirPath) {
    return new Promise((resolve, reject) => {
      plus.io.resolveLocalFileSystemURL(
        `_doc/${dirPath}`,
        entry => resolve(entry),
        error => resolve(null)
      )
    })
  }
  
  // 创建目录
  async createDir(dirPath) {
    return new Promise((resolve, reject) => {
      plus.io.resolveLocalFileSystemURL(
        '_doc',
        rootEntry => {
          rootEntry.getDirectory(
            dirPath,
            { create: true, exclusive: false },
            dirEntry => resolve(dirEntry),
            error => reject(error)
          )
        },
        error => reject(error)
      )
    })
  }
  
  // 获取文件信息
  async getFileInfo(filePath) {
    return new Promise((resolve, reject) => {
      plus.io.resolveLocalFileSystemURL(
        `_doc/${filePath}`,
        entry => resolve(entry),
        error => resolve(null)
      )
    })
  }
  
  // 写入文件
  async writeFile(filePath, content) {
    return new Promise((resolve, reject) => {
      plus.io.resolveLocalFileSystemURL(
        '_doc',
        rootEntry => {
          rootEntry.getFile(
            filePath,
            { create: true, exclusive: false },
            fileEntry => {
              fileEntry.createWriter(
                writer => {
                  writer.onwriteend = () => resolve()
                  writer.onerror = e => reject(e)
                  writer.write(content)
                },
                error => reject(error)
              )
            },
            error => reject(error)
          )
        },
        error => reject(error)
      )
    })
  }
  
  // 追加内容到文件
  async appendFile(filePath, content) {
    return new Promise((resolve, reject) => {
      plus.io.resolveLocalFileSystemURL(
        `_doc/${filePath}`,
        fileEntry => {
          fileEntry.createWriter(
            writer => {
              writer.onwriteend = () => resolve()
              writer.onerror = e => reject(e)
              writer.seek(writer.length)
              writer.write(content)
            },
            error => reject(error)
          )
        },
        error => reject(error)
      )
    })
  }
  
  // 读取文件内容
  async readFile(filePath) {
    return new Promise((resolve, reject) => {
      plus.io.resolveLocalFileSystemURL(
        `_doc/${filePath}`,
        fileEntry => {
          fileEntry.file(
            file => {
              const reader = new plus.io.FileReader()
              reader.onloadend = e => resolve(e.target.result)
              reader.onerror = e => reject(e)
              reader.readAsText(file)
            },
            error => reject(error)
          )
        },
        error => reject(error)
      )
    })
  }
  
  // 读取目录内容
  async readDir(dirPath) {
    return new Promise((resolve, reject) => {
      plus.io.resolveLocalFileSystemURL(
        `_doc/${dirPath}`,
        dirEntry => {
          const reader = dirEntry.createReader()
          reader.readEntries(
            entries => resolve(entries),
            error => reject(error)
          )
        },
        error => reject(error)
      )
    })
  }
  
  // 删除文件
  async deleteFile(filePath) {
    return new Promise((resolve, reject) => {
      plus.io.resolveLocalFileSystemURL(
        `_doc/${filePath}`,
        entry => {
          entry.remove(
            () => resolve(),
            error => reject(error)
          )
        },
        error => reject(error)
      )
    })
  }
}

// 创建全局单例
const logger = new Logger()

export default logger

使用方法

在 UniApp 项目中使用该日志工具非常简单,以下是使用示例:

import logger from './logger.js'

// 记录不同级别日志
logger.debug('这是一条调试信息', { key: 'value' })
logger.info('这是一条普通信息')
logger.warn('这是一条警告信息')
logger.error('这是一条错误信息', new Error('示例错误'))

// 设置日志最大保存天数
logger.setMaxLogDays(30) // 保存30天日志

// 手动清理日志
logger.cleanOldLogs()

// 清除所有日志
logger.clearAllLogs()

使用时,只需在项目中引入该方法,即可在任何需要记录日志的地方调用相应方法。

### UniApp APP文件流下载实现方法 在UniApp中,APP的文件流下载可以通过`uni.downloadFile()`接口来完成。该接口允许开发者指定目标文件的URL,并返回一个临时路径供后续操作使用[^1]。 以下是完整的代码示例: ```javascript // 定义下载函数 function downloadFile(url) { uni.downloadFile({ url: url, // 替换为目标文件的实际URL success: (res) => { if (res.statusCode === 200) { const tempFilePath = res.tempFilePath; console.log('文件已成功下载至:', tempFilePath); // 如果需要保存到本地存储,可以调用以下API saveToFileSystem(tempFilePath); } }, fail: (err) => { console.error('文件下载失败:', err); } }); } // 将临时文件保存到本地存储 function saveToFileSystem(filePath) { uni.saveFile({ tempFilePath: filePath, success: (saveRes) => { const savedPath = saveRes.savedFilePath; console.log('文件已保存至本地:', savedPath); }, fail: (saveErr) => { console.error('文件保存失败:', saveErr); } }); } ``` #### 关键点解析 1. **`uni.downloadFile()`** 此接口用于从远程服务器下载文件并将其存放在应用沙盒中的临时目录下。通过回调参数`tempFilePath`获取文件的临时路径[^1]。 2. **错误处理** 需要特别注意网络状态以及权限设置。如果遇到下载失败的情况,应捕获错误日志以便排查问题[^2]。 3. **保存到本地存储** 使用`uni.saveFile()`将临时文件移动到持久化存储区域。这一步对于大文件尤其重要,因为临时文件可能随时被清理掉[^1]。 4. **跨平台兼容性** 不同运行环境(H5、小程序、原生APP)对文件系统的支持存在差异。因此,在实际开发过程中需利用条件编译语法适配各终行为[^3]。 --- ### 注意事项 - 确保项目配置了必要的权限声明,例如访问外部存储设备。 - 对于PDF等特殊格式的内容展示,建议结合WebView组件加载专用阅读器页面[^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jiaberrrr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值