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

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

功能需求分析

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

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

实现方案

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

javascript 复制代码
// 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 项目中使用该日志工具非常简单,以下是使用示例:

javascript 复制代码
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()

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