Electron 本地文件与系统交互

使用文件对话框、异步文件 API 和常用系统能力完成安全的本地文件操作。

🎯 引言

这篇文章只完成一个功能:点击按钮选择一个 .txt 文件,并把文件内容显示到页面中

学完本篇,你将能够:

  • 使用 dialog.showOpenDialog 打开系统文件选择框。
  • 取得用户选择的文件路径。
  • 使用 readFile 读取文本文件。
  • 把文件名和文件内容显示到页面中。

🧱 选择文件和读取文件的区别

选择文件和读取文件是两个不同的操作:

操作使用的 API得到什么
选择文件dialog.showOpenDialog文件路径
读取文件readFile文件中的内容

showOpenDialog 只负责让用户选择文件,它不会自动读取文件内容。取得路径后,还要把路径交给 readFile

这两个操作都写在 main.js 中。页面只调用预加载脚本提供的 openTextFile(),不直接接触 Electron 和 Node.js 模块。


🛠 在主进程选择并读取文件

下面是完整的 main.js。请替换原文件,不要把它追加到已有代码后面。

同一个频道只能注册一次。main.js 中只能保留一个 ipcMain.handle('file:open-text', ...),否则会出现 Attempted to register a second handler
main.js
const { app, BrowserWindow, dialog, ipcMain } = require('electron/main');
const { readFile } = require('node:fs/promises');
const path = require('node:path');

function createWindow() {
    const win = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            preload: path.join(__dirname, 'preload.js'),
        },
    });

    win.loadFile(path.join(__dirname, 'index.html'));
}

async function openTextFile() {
    const result = await dialog.showOpenDialog({
        title: '选择文本文件',
        properties: ['openFile'],
        filters: [
            {
                name: '文本文件',
                extensions: ['txt'],
            },
        ],
    });

    if (result.canceled || result.filePaths.length === 0) {
        return null;
    }

    const filePath = result.filePaths[0];
    const content = await readFile(filePath, 'utf8');

    return {
        name: path.basename(filePath),
        content,
    };
}

app.whenReady().then(() => {
    ipcMain.handle('file:open-text', openTextFile);
    createWindow();

    app.on('activate', () => {
        if (BrowserWindow.getAllWindows().length === 0) {
            createWindow();
        }
    });
});

app.on('window-all-closed', () => {
    if (process.platform !== 'darwin') {
        app.quit();
    }
});

先看打开对话框的部分:

  • properties: ['openFile']:只能选择文件。
  • extensions: ['txt']:只显示 .txt 文件。
  • result.canceled:用户是否取消选择。
  • result.filePaths[0]:用户选择的第一个文件路径。

取得路径后,下面这行代码读取文件内容:

const content = await readFile(filePath, 'utf8');

'utf8' 表示按 UTF-8 编码读取文本。path.basename(filePath) 则从完整路径中取得文件名。

Windows、macOS 和 Linux 的文件选择框外观可能不同,实际效果以当前操作系统为准。

🌉 给页面提供 openTextFile

main.js 已经能选择和读取文件,接下来在 preload.js 中给页面提供调用方法:

preload.js
const { contextBridge, ipcRenderer } = require('electron/renderer');

contextBridge.exposeInMainWorld('electronAPI', {
    openTextFile: () => ipcRenderer.invoke('file:open-text'),
});

页面最终可以调用:

window.electronAPI.openTextFile();

它会请求 main.js 中同名频道 file:open-text 的处理函数,并等待文件数据返回。

这里只给页面提供 openTextFile(),不要把完整的 ipcRendererfs 模块暴露给页面。

🖥 准备页面

index.html 中添加一个按钮,以及显示文件名和内容的位置:

index.html
<!doctype html>
<html lang="zh-CN">
    <head>
        <meta charset="UTF-8" />
        <meta
            http-equiv="Content-Security-Policy"
            content="default-src 'self'; script-src 'self'"
        />
        <title>文本文件预览</title>
    </head>
    <body>
        <h1>文本文件预览</h1>
        <button id="open-file" type="button">选择文件</button>
        <p id="file-name">还没有选择文件</p>
        <pre id="file-content"></pre>

        <script src="./renderer.js"></script>
    </body>
</html>

pre 元素会保留文本中的换行和空格,适合预览纯文本文件。


🖱 点击按钮并显示内容

最后在 renderer.js 中处理按钮点击:

renderer.js
const openButton = document.querySelector('#open-file');
const fileName = document.querySelector('#file-name');
const fileContent = document.querySelector('#file-content');

openButton.addEventListener('click', async () => {
    try {
        const file = await window.electronAPI.openTextFile();

        if (!file) {
            fileName.textContent = '已取消选择';
            return;
        }

        fileName.textContent = file.name;
        fileContent.textContent = file.content;
    } catch (error) {
        console.error('读取文件失败:', error);
        fileName.textContent = '读取文件失败';
        fileContent.textContent = '';
    }
});

这里需要处理三种结果:

  • 用户选择文件:显示文件名和内容。
  • 用户取消选择:显示“已取消选择”。
  • 文件读取失败:显示错误提示。

文件内容使用 textContent 显示。即使文本中包含 HTML 标签,也只会显示为普通文字。

运行项目:

npm start

点击“选择文件”,选择一个 UTF-8 编码的 .txt 文件,页面就会显示文件名和内容。


🪤 常见问题

出现 Attempted to register a second handler

这表示 main.js 中重复注册了 file:open-text。搜索这个频道名,只保留一个 ipcMain.handle;直接使用本文的完整 main.js 替换旧文件即可避免重复。

点击按钮后没有反应

先检查 BrowserWindow 是否正确配置了 preload.js

main.js
const win = new BrowserWindow({
    webPreferences: {
        preload: path.join(__dirname, 'preload.js'),
    },
});

还要确认预加载脚本和主进程使用了相同的频道名:file:open-text

取消选择后为什么没有文件数据

取消属于正常操作。openTextFile() 会返回 null,页面判断 !file 后直接结束即可。

为什么看不到 Markdown 文件

当前 filters 只包含 txt。如果还要选择 Markdown 文件,可以改成:

extensions: ['txt', 'md'],

扩展名不要写点号,应写成 'md',而不是 '.md'

为什么中文内容显示乱码

本例按照 UTF-8 读取文件。如果原文件使用其他编码,需要先把文件转换为 UTF-8,或使用对应的解码方式。


🧾 小节总结

  • showOpenDialog 负责打开系统文件选择框。
  • showOpenDialog 返回路径,不会自动读取文件。
  • readFile(filePath, 'utf8') 用于读取文本内容。
  • 用户取消选择时,应该正常结束,不需要显示错误。
  • 预加载脚本只向页面提供用途明确的 openTextFile()
  • 页面使用 textContent 显示文件内容。

❓ 知识问答

Q1:为什么 filePaths 是数组?

A:因为 showOpenDialog 支持多选。本文只选择一个文件,所以读取第一个元素。

Q2:filters 能证明文件一定是文本吗?

A:不能。它只按扩展名筛选文件,不能检查文件内部的内容。

Q3:为什么 readFile 要写 utf8?

A:因为本文读取的是文本。指定 utf8 后,返回值才是字符串。

Q4:为什么 dialog 写在 main.js 中?

A:dialog 是主进程模块,不能直接在普通页面脚本中使用。


🧪 小练习

练习一:让文件选择框同时支持 .txt.md 文件。

filters: [
    {
        name: '文本文件',
        // 请在这里编写代码
    },
],

练习二:成功读取文件后,把页面标题改成文件名。

renderer.js
if (file) {
    // 请在这里编写代码
}

🎉 恭喜你已经完成 Electron 文本文件选择与读取功能啦!