8、文件系统使用场景(Electron)

1、章节目标

文件系统,是electron业务开发非常重要的一个模块,相关的api、使用场景,都会更丰富一些。

本章节,我们会总结几个常用的业务场景,结合文件相关的api,一起来实现相关功能。

  • 熟悉文件系统相关的api
  • 熟悉文件系统常用的一些业务场景
  • 通过业务场景,反向理解文件系统相关api

2、相关的api

相关的api,我们虽然上个章节已经介绍过,但是我们再次简单总结一下。

  • fs,是我们的文件处理的api,可以读取/写入相关的具体业务内容
  • dialog,系统提供的文件对话框,可以选择文件,保存文件,得到相关文件的路径
  • path,一般而言,path是一种跨平台的的路径处理方式
  • app.getPath,获取相关模块的文件路径

3、文件系统使用场景

场景1、应用配置管理

需求说明:在桌面端应用开发的过程中,有时候我们想让代码本地,存储一些配置文件,方便我们业务的开展。比如,我们想在本地记录我们的应用的版本信息,然后根据不同的版本,业务上我们可能有不同的处理方式。特别是,不同的版本对比,然后走加载新版本的代码的业务流程,加载了新的版本,然后记录,更新本地的最新版本号。

ipcMain.handle("config:load", async () => {
    // path.join,把两个路径拼接到一起
    // app.getPath,获取本地数据
  const configPath = path.join(app.getPath("userData"), "config.json");
  try {
    // fs.readFile,读取文件内容
    const data = await fs.readFile(configPath, "utf8");
    return { success: true, config: JSON.parse(data) };
  } catch (error) {
    // 配置文件不存在时返回默认配置
    return { success: true, config: { theme: "light", language: "zh-CN" } };
  }
});

ipcMain.handle("config:save", async (_e, config: any) => {
  const configPath = path.join(app.getPath("userData"), "config.json");
  try {
    // fs.writeFile,写入文件内容
    await fs.writeFile(configPath, JSON.stringify(config, null, 2));
    return { success: true };
  } catch (error) {
    return { success: false, error: error.message };
  }
});

场景2、数据持久化存储

数据的存储和读取,永远是一个应用软件的核心能力。

对于前端,我们的localstorage的api,就是提供数据存储的能力的。

那对于桌面端应用,主进程的数据的持久化存储,应该存放到标准的目录结构,在真实的业务场景中,一般就是path.join(app.getPath("userData"), "data", `xxx.json`),这样的结构。

// 本地数据库文件
ipcMain.handle("data:save", async (_e, table: string, data: any[]) => {
  // 构造文件路径,文件名为table的变量,存储到data目录中。
  const dataPath = path.join(app.getPath("userData"), "data", `${table}.json`);
  try {
    // 确保目录存在
    await fs.mkdir(path.dirname(dataPath), { recursive: true });
    // 把真实的数据data,写入到dataPath中
    await fs.writeFile(dataPath, JSON.stringify(data, null, 2));
    return { success: true };
  } catch (error) {
    return { success: false, error: error.message };
  }
});

ipcMain.handle("data:load", async (_e, table: string) => {
  const dataPath = path.join(app.getPath("userData"), "data", `${table}.json`);
  try {
    // 读取文件,文件名为table
    const data = await fs.readFile(dataPath, "utf8");
    return { success: true, data: JSON.parse(data) };
  } catch (error) {
    return { success: false, error: error.message };
  }
});

场景3、日志记录系统

有了本地存储,在某些业务中,也会有本地日志。比如,在软件运行报错、异常关闭等非常规的场景中,希望有日志记录在本地,就会使用到我们的文件系统。

在以下的代码案例中,实现了以天为单位记录日志。

new Date().toISOString().split("T")[0],是一个类似2025-10-04格式的字符串,logFile是使用path.join的方式,进行文件路径的拼接。

每次wrire操作,就是向日志文件中写入内容,其核心api就是:fs.appendFile(logFile, logEntry)。

ipcMain.handle("log:write", async (_e, level: string, message: string) => {
  const logDir = path.join(app.getPath("userData"), "logs");
    // 定义文件名称,以天为单位,其中new Date().toISOString().split("T")[0],是一个类似2025-10-04格式的字符串。
  const logFile = path.join(
    logDir,
    `${new Date().toISOString().split("T")[0]}.log`
  );

  try {
    // 确保文件目录存储
    await fs.mkdir(logDir, { recursive: true });
    const timestamp = new Date().toISOString();
    const logEntry = `[${timestamp}] ${level.toUpperCase()}: ${message}\n`;
    // fs.appendFile,表示将log内容,写入到logFile中。
    await fs.appendFile(logFile, logEntry);
    return { success: true };
  } catch (error) {
    return { success: false, error: error.message };
  }
});

场景4、数据备份

数据备份,在一些特殊的业务场景中,是一个场景操作。

比如,在网盘中,我们经常的一个操作就是:数据备份。

数据备份,核心点就是,把某个目录的文件,原封不动的存储到另一个目录中。

以下代码中:copyDirectory函数实现了,文件copy操作,备份的文件就是app.getPath("userData")下的所有文件和文件夹。备份文件存储的目录为:path.join(app.getPath("documents"), "AppBackups")。

// 数据备份
ipcMain.handle("backup:create", async (_e, backupName?: string) => {
  const backupDir = path.join(app.getPath("documents"), "AppBackups");
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
  const backupName = backupName || `backup_${timestamp}`;
  const backupPath = path.join(backupDir, backupName);

  try {
    await fs.mkdir(backupPath, { recursive: true });

    // 复制用户数据
    const userDataPath = app.getPath("userData");
    await copyDirectory(userDataPath, backupPath);

    return { success: true, backupPath };
  } catch (error) {
    return { success: false, error: error.message };
  }
});

// 辅助函数:复制目录
async function copyDirectory(src: string, dest: string) {
  await fs.mkdir(dest, { recursive: true });
  const items = await fs.readdir(src, { withFileTypes: true });

  for (const item of items) {
    const srcPath = path.join(src, item.name);
    const destPath = path.join(dest, item.name);

    if (item.isDirectory()) {
      await copyDirectory(srcPath, destPath);
    } else {
      await fs.copyFile(srcPath, destPath);
    }
  }
}

场景5、文件的导入和导出

数据的导入导出,是一个高频场景。

它主要包括两方面的诉求,一方面是把数据从主体应用中,写入到我们的本地文件里。另一方面,就是把本地的文件进行读取,然后把获取的数据,提供给我们的应用使用。

以下就是一个基本的数据的导入导出的功能。

// 数据导出(数据写入本地文件)
ipcMain.handle(
  "export:data",
  async (_e, data: any, format: "json" | "csv" = "json") => {
    const { canceled, filePath } = await dialog.showSaveDialog({
      defaultPath: `export_${new Date().toISOString().split("T")[0]}.${format}`,
      filters: [
        { name: "JSON Files", extensions: ["json"] },
        { name: "CSV Files", extensions: ["csv"] },
      ],
    });

    if (canceled) return { success: false, error: "Export canceled" };

    try {
      let content: string;
      if (format === "json") {
        content = JSON.stringify(data, null, 2);
      } else {
        // CSV 转换逻辑
        content = convertToCSV(data);
      }

      await fs.writeFile(filePath, content, "utf8");
      return { success: true, filePath };
    } catch (error) {
      return { success: false, error: error.message };
    }
  }
);

// 数据导入(把文件中的数据读取并提供给应用使用)
ipcMain.handle("import:data", async () => {
  const { canceled, filePaths } = await dialog.showOpenDialog({
    properties: ["openFile"],
    filters: [
      { name: "JSON Files", extensions: ["json"] },
      { name: "CSV Files", extensions: ["csv"] },
    ],
  });

  if (canceled || filePaths.length === 0)
    return { success: false, error: "Import canceled" };

  try {
    const content = await fs.readFile(filePaths[0], "utf8");
    const ext = path.extname(filePaths[0]).toLowerCase();

    let data: any;
    if (ext === ".json") {
      data = JSON.parse(content);
    } else if (ext === ".csv") {
      data = parseCSV(content);
    }

    return { success: true, data };
  } catch (error) {
    return { success: false, error: error.message };
  }
});

以上5个使用场景,就是electron文件处理相关api的应用。

其实从整个技能体系来说,以上的内容,准确的说是属于node的知识,electron和node的关系,更像是electron提供了一套基于node的主进程能力。

4、结语

技术学习是一场持续的旅程,我很荣幸能与你同行。关注这个专栏,让我们共同探索 Electron 的奥秘,在实践中成长,在交流中进步。期待与你一起,从基础概念到项目实战,一步步构建出优秀的桌面应用程序!

关于本栏目

本专栏的所有内容和代码,github地址:https://github.com/techLaoLi/electron-tutorial

关于作者,前大厂技术专家,现大前端技术负责人,公众号:老李说技术。

欢迎大家的关注。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

老李说技术

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

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

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

打赏作者

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

抵扣说明:

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

余额充值