Electron 应用接入本地日志功能指南

974 阅读8分钟

大家好,我是徐徐。今天我们要讲的是如何在 Electron 应用中接入日志功能。

前言

在我们的日常开发 Electron 应用的过程中,肯定会遇到如何打印日志的问题,不可能全部都使用 console.log 的方法去打印日志,主要是 console.log 无法把日志存入文件中,到后面如果要分析客户端的日志是非常不好下手的。另外就是要开发一款完整的客户端应用,我觉得在开发环境搭建好了情况下,我们就应该在第一步接入日志功能,一个好的日志功能可以帮你分析和定位一些表象无法找到的问题,帮助你快速解决问题。那么下面我们就来看看 Electron 如何实现一个好用的日志功能吧。

日志工具库比较

我们在开始接入日志功能的时候肯定是需要做技术选型的,一款合适的日志工具库上手起来非常的快速和方便。比较常见的日志工具就是 electron-log 和 log4js-node,这两款日志工具我都有用过。我们可以做一下对比:

log4js 的下载量的确比 electron-log 的高,可能也是因为 log4js 是在 node 应用里面使用,electron-log 大部分是在 electron 中使用。下面是一个简单的比较。

特性electron-loglog4js
定位专为 Electron 应用设计,简单易用通用日志库,适用于各种 Node.js 项目
包体积89.2k499k
安装和配置简单,适合快速集成配置灵活,功能丰富,适合复杂需求
日志存储支持本地文件存储、远程传输、日志文件大小限制支持多种存储方式(文件、数据库、网络等)
灵活性配置简单,适合基本需求高度灵活,支持复杂的配置和多种输出方式
性能性能较好,适合一般日志需求性能稍逊,适合复杂日志系统但需要优化
错误捕获支持错误日志远程发送需要配置,支持更复杂的错误捕获和输出方式
使用场景小到中型 Electron 应用,快速集成日志需要复杂日志管理和输出的企业级应用

我在这里会实现两个日志工具的应用供大家参考,大家可根据自己的场景去做应用。

electron-log 接入

先看下使用文档:

github.com/megahertz/e…

文档很简单,而且分别有 main 和 render 以及 node 场景下的日志功能。

不过这里有一个小问题,就是

import log from 'electron-log/renderer';
log.info('Log from the renderer process');

如果主进程没有初始化 electron-log 的话,会报错 electron-log: logger isn't initialized in the main process。

所以为了避免这种情况的发生,我们还是需要自己对 electron-log 再封装一层,以便更好的满足实用场景。当然底层我们还是用 preload 实现。

核心思路是:构建 ElectronLogger 类,提供一个可供渲染进程和主进程使用的 log 方法,注入 preload 脚本并暴露给渲染进程,抹平两个进程的调用差异。

构建 ElectronLogger 类

  • src/common/log/api.ts
class ElectronLogger {
  private static instance: ElectronLogger | null = null;
  private logger: any;

  private constructor() {}

  private async initialize() {
    try {
      const log = require("electron-log/main")
      log.initialize();
      this.logger = log;
    } catch (error) {
      console.error("Failed to initialize logger:", error);
      this.logger = null;
    }
  }

  static getInstance(): ElectronLogger {
    if (ElectronLogger.instance === null) {
      ElectronLogger.instance = new ElectronLogger();
      ElectronLogger.instance.initialize();
    }
    return ElectronLogger.instance;
  }

  info(message: string): void {
    this.logger?.info(message);
  }

  warn(message: string): void {
    this.logger?.warn(message);
  }

  error(message: string): void {
    this.logger?.error(message);
  }

  debug(message: string): void {
    this.logger?.debug(message);
  }
}

export { ElectronLogger };

这里为了方便演示就先暴露 ElectronLogger ,因为后面还要演示 log4js。这里我们需要注意的是在 initialize 的时候,是在内部 require("electron-log/main"),而不是在外部 import ,这样做的主要目的是避免渲染进程可能会依赖一些不必要的系统级依赖,比如文件系统等,外部导入导致报错。

提供可供渲染进程和主进程使用的公用 log 方法

  • src/common/log/index.ts
import { ElectronLogger } from "./api";

export type LOG_TYPE = 'error' | 'warn' | 'info' | 'debug'
export interface LOG_PARAMS {
  type: LOG_TYPE
  value: string
}

let loggerInstance: ElectronLogger | null = null;

const ElectronLoggerInstance = () => {
  if (!loggerInstance) {
    loggerInstance = ElectronLogger.getInstance();
  }
  return loggerInstance;
};

export const Elog = {
  info: (value: string) => {
    if (import.meta.env.VITE_CURRENT_RUN_MODE === "render") {
      window.electronAPI.Elog('info',value)
      console.info(value)
    } else {
      ElectronLoggerInstance().info(value)
    }
  },
  warn: (value: string) => {
    if (import.meta.env.VITE_CURRENT_RUN_MODE === "render") {
      window.electronAPI.Elog('warn',value)
      console.warn(value)
    } else {
      ElectronLoggerInstance().warn(value)
    }
  },
  error: (value: string) => {
    if (import.meta.env.VITE_CURRENT_RUN_MODE === "render") {
      window.electronAPI.Elog('error',value)
      console.error(value)
    } else {
      ElectronLoggerInstance().error(value)
    }
  },
  debug: (value: string) => {
    if (import.meta.env.VITE_CURRENT_RUN_MODE === "render") {
      window.electronAPI.Elog('debug',value)
      console.debug(value)
    } else {
      ElectronLoggerInstance().debug(value)
    }
  }
}

根据环境区分不同的调用方法,然后就可以在应用内各处随意调用了,不过还差最后一步,那就是注入 preload 脚本并暴露给渲染进程。

注入 preload 脚本并暴露给渲染进程

  • src/preload/index.ts
Elog: (type: LOG_TYPE, value: string) => {
    ipcRenderer.send('Elog', { type, value })
}
  • src/main/ipc/index.ts
ipcMain.on('Elog', (event:IpcMainEvent,arg: LOG_PARAMS) => {
      const { type, value } = arg
      switch (type) {
        case 'info':
          Elog.info(value)
          break
        case 'error':
          Elog.error(value)
          break
        case 'warn':
          Elog.warn(value)
          break
        case 'debug':
          Elog.debug(value)
          break
        default:
          console.log('Unknown log type:', type, ...value)
          break
      }
    })
}

经过上面的步骤,我们就完成了 electron-log 的接入,然后你就可以随处使用你的日志工具了。

log4js 接入

log4js 的接入和 electron-log 也差不多,但是配置的话可能更加多样化一点,自定义空间更加广泛。

先看下使用文档:

log4js-node.github.io/log4js-node…

依旧还是三个步骤。

构建 Logger4 类

  • src/common/log/api.ts
class Logger4 {
  private static instance: Logger4 | null = null;
  private logger: any;

  private constructor() {
    this.initLogger();
  }

  public static getInstance(): Logger4 {
    if (this.instance === null) {
      this.instance = new Logger4();
    }
    return this.instance;
  }

  private getLogPath(): string {
    const { app } = require("electron");
    const path = require("path");
    const userDataPath = app.getPath("userData");
    const logPath = path.join(userDataPath, "logs");
    return logPath;
  }

  private initLogger() {
    const log4js = require("log4js");
    const path = require("path");
    const logPath = this.getLogPath();
    
    try {
      // 配置 log4js,指定日志输出方式和文件存储设置
      log4js.configure({
        appenders: {
          // 控制台输出配置
          out: {
            type: "console",  // 日志输出到控制台
          },
          // 日志文件输出配置
          main: {
            type: "dateFile",  // 使用日期文件类型的 appender
            filename: path.join(logPath, "log"),  // 日志文件的路径和名称
            pattern: "yyyy-MM-dd.log",  // 日志文件名的日期模式
            alwaysIncludePattern: true,  // 文件名总是包含模式中的日期
            maxLogSize: 3 * 1024 * 1024,  // 单个日志文件的最大尺寸(3 MB)
            backups: 20,  // 保留的日志文件备份数量
            mode: 0o777,  // 文件权限模式
          },
        },
        categories: {
          // 默认的日志类别及其关联的 appenders 和日志级别
          default: {
            appenders: ["out", "main"],  // 同时输出到控制台和日志文件
            level: "debug",  // 日志级别为 debug,记录 debug 及以上级别的日志
          },
        },
      });
      // 获取名为 "main" 的 logger 实例
      this.logger = log4js.getLogger("main");
    } catch (err) {
      // 捕获并输出初始化日志系统时的错误
      console.error("initLogger error:", err);
    }
  }

  public info(message: string) {
    if (this.logger) {
      this.logger.info(message);
    } else {
      console.log("Logger not initialized:", message);
    }
  }

  public error(message: string) {
    if (this.logger) {
      this.logger.error(message);
    } else {
      console.log("Logger not initialized:", message);
    }
  }

  public warn(message: string) {
    if (this.logger) {
      this.logger.warn(message);
    } else {
      console.log("Logger not initialized:", message);
    }
  }

  public debug(message: string) {
    if (this.logger) {
      this.logger.debug(message);
    } else {
      console.log("Logger not initialized:", message);
    }
  }
}

大体的思路跟上面 ElectronLogger 的构建差不多,主要是 initLogger ,一些配置我也都给了相应的注释。

提供可供渲染进程和主进程使用的公用 log 方法

同 electron-log 相关的方法,这里多了一个 parseLog 方法,主要是把一些非字符串的内容转换一下。

  • src/common/log/index.ts
const parseLog = (params: any[]): string => {
  return params.map(param => {
    if (typeof param === 'string') {
      return param;
    } else if (param instanceof Error) {
      return param.stack || param.message;
    } else {
      return JSON.stringify(param);
      JSON.parse
    }
  }).join(" ");
}

export const Log4 = {
  info: (value: any) => {
    if (import.meta.env.VITE_CURRENT_RUN_MODE === "render") {
      window.electronAPI.Log4('info',parseLog(value))
      console.info(value)
    } else {
      Logger4Instance().info(value)
    }
  },
  warn: (value: any) => {
    if (import.meta.env.VITE_CURRENT_RUN_MODE === "render") {
      window.electronAPI.Log4('warn',parseLog(value))
      console.warn(value)
    } else {
      Logger4Instance().warn(value)
    }
  },
  error: (value: any) => {
    if (import.meta.env.VITE_CURRENT_RUN_MODE === "render") {
      window.electronAPI.Log4('error',parseLog(value))
      console.error(value)
    } else {
      Logger4Instance().error(value)
    }
  },
  debug: (value: any) => {
    if (import.meta.env.VITE_CURRENT_RUN_MODE === "render") {
      window.electronAPI.Log4('debug',parseLog(value))
      console.debug(value)
    } else {
      Logger4Instance().debug(value)
    }
  }
}

注入 preload 脚本并暴露给渲染进程

同 electron-log 相关的方法。

  • src/preload/index.ts
Log4: (type: LOG_TYPE, value: string) => {
    ipcRenderer.send('Log4', { type, value })
  }
  • src/main/ipc/index.ts
 ipcMain.on('Log4', (event:IpcMainEvent,arg: LOG_PARAMS) => {
      const { type, value } = arg
      switch (type) {
        case 'info':
          Log4.info(value)
          break
        case 'error':
          Log4.error(value)
          break
        case 'warn':
          Log4.warn(value)
          break
        case 'debug':
          Log4.debug(value)
          break
        default:
          console.log('Unknown log type:', type, ...value)
          break
      }
    })

结语

在本文中,我们详细介绍了如何在 Electron 应用中集成日志功能,包括使用 electron-loglog4js-node 两款常见的日志工具库。通过对比它们的特性和使用场景,我们能够更好地选择适合自己项目的日志工具。我们还通过实际代码示例展示了如何封装日志工具库,并提供统一的日志接口供主进程和渲染进程调用,实现了对日志功能的高效管理。这里只是做了最简单的演示,已满足基本需求,大家可以再此基础上自行扩展以适应自己的应用。

集成日志功能是 Electron 应用开发中的一个重要环节,一个健全的日志系统不仅能够帮助开发者快速定位和解决问题,还可以在应用运行过程中实时监控和记录重要事件,确保应用的稳定性和可维护性。在实际开发中,根据项目的具体需求选择合适的日志工具,并做好日志的配置和管理,可以大大提升开发效率和应用质量。

希望本文对大家在 Electron 应用中集成日志功能有所帮助,如果有任何问题或建议,欢迎交流和讨论!

源码

github.com/Xutaotaotao…

开源项目

一站式Electron开发解决方案 Electron-Prokitgithub.com/Xutaotaotao…