Electron 文件选择功能实战指南适配鸿蒙

Electron 文件选择功能实战指南

目录


功能概述

文件选择是桌面应用中最常见的功能之一。在 Electron 中实现文件选择功能,需要:

  1. 主进程 :使用 dialog.showOpenDialog 打开系统文件选择对话框
  2. IPC 通信:渲染进程通过 IPC 请求主进程打开对话框
  3. 文件信息读取 :使用 Node.js fs 模块获取文件详细信息
  4. UI 展示:在界面上展示选中的文件信息

实现效果

  • ✅ 点击按钮打开系统文件选择对话框
  • ✅ 支持多种文件类型过滤(所有文件、文本、图片、视频)
  • ✅ 显示文件完整路径
  • ✅ 显示文件大小(自动格式化)
  • ✅ 显示文件类型(扩展名)
  • ✅ 显示最后修改时间

技术架构

进程通信流程

复制代码
渲染进程 (index.html)
    ↓
点击"选择文件"按钮
    ↓
ipcRenderer.invoke('select-file')
    ↓
主进程 (main.js)
    ↓
ipcMain.handle('select-file')
    ↓
dialog.showOpenDialog()
    ↓
用户选择文件
    ↓
返回文件路径
    ↓
渲染进程接收结果
    ↓
读取文件信息并显示

关键技术点

  1. IPC 通信 :使用 ipcRenderer.invoke()ipcMain.handle() 实现异步通信
  2. 对话框 API :使用 Electron 的 dialog 模块打开系统原生对话框
  3. 文件系统 :使用 Node.js fs 模块读取文件信息
  4. 路径处理 :使用 path 模块处理文件路径

主进程实现

1. 引入必要模块

javascript 复制代码
const { app, BrowserWindow, ipcMain, dialog } = require('electron')
const path = require('path')
  • ipcMain:处理来自渲染进程的 IPC 消息
  • dialog:显示系统对话框(文件选择、保存等)

2. 注册 IPC 处理器

javascript 复制代码
// 处理文件选择请求
ipcMain.handle('select-file', async () => {
  const window = BrowserWindow.getFocusedWindow() || mainWindow
  const result = await dialog.showOpenDialog(window, {
    properties: ['openFile'],
    filters: [
      { name: '所有文件', extensions: ['*'] },
      { name: '文本文件', extensions: ['txt', 'md', 'json'] },
      { name: '图片文件', extensions: ['jpg', 'jpeg', 'png', 'gif', 'bmp'] },
      { name: '视频文件', extensions: ['mp4', 'avi', 'mov', 'mkv'] }
    ]
  })
  
  if (!result.canceled && result.filePaths.length > 0) {
    return {
      success: true,
      filePath: result.filePaths[0]
    }
  }
  
  return {
    success: false,
    filePath: null
  }
})

3. 关键参数说明

dialog.showOpenDialog() 参数

window:父窗口对象

  • BrowserWindow.getFocusedWindow():获取当前聚焦的窗口
  • mainWindow:回退到主窗口(如果没有聚焦窗口)

properties:对话框属性

  • ['openFile']:选择单个文件
  • ['openFile', 'multiSelections']:选择多个文件
  • ['openDirectory']:选择目录

filters:文件类型过滤器

javascript 复制代码
filters: [
  { name: '显示名称', extensions: ['扩展名1', '扩展名2'] }
]

4. 返回值处理

javascript 复制代码
{
  canceled: false,        // 是否取消
  filePaths: ['/path/to/file']  // 选中的文件路径数组
}

渲染进程实现

1. 引入必要模块

javascript 复制代码
const os = require('os');
const fs = require('fs');
const path = require('path');
const { ipcRenderer } = require('electron');

2. 文件选择处理

javascript 复制代码
selectFileBtn.addEventListener('click', async () => {
  try {
    const result = await ipcRenderer.invoke('select-file');
    
    if (result.success && result.filePath) {
      const fileInfoData = getFileInfo(result.filePath);
      
      if (fileInfoData) {
        filePathEl.textContent = fileInfoData.path;
        fileSizeEl.textContent = formatFileSize(fileInfoData.size);
        fileTypeEl.textContent = fileInfoData.type;
        fileModifiedEl.textContent = formatDate(fileInfoData.modified);
        fileInfo.classList.add('show');
      } else {
        alert('无法读取文件信息');
      }
    } else {
      console.log('用户取消了文件选择');
    }
  } catch (error) {
    console.error('选择文件时出错:', error);
    alert('选择文件时出错: ' + error.message);
  }
});

3. 文件信息读取

javascript 复制代码
function getFileInfo(filePath) {
  try {
    const stats = fs.statSync(filePath);
    const ext = path.extname(filePath).toLowerCase();
    
    return {
      path: filePath,
      size: stats.size,
      type: ext || '未知类型',
      modified: stats.mtime
    };
  } catch (error) {
    console.error('获取文件信息失败:', error);
    return null;
  }
}

fs.statSync() 返回的文件信息:

  • size:文件大小(字节)
  • mtime:最后修改时间
  • birthtime:创建时间
  • isFile():是否为文件
  • isDirectory():是否为目录

4. 格式化工具函数

文件大小格式化
javascript 复制代码
function formatFileSize(bytes) {
  if (bytes === 0) return '0 B';
  const k = 1024;
  const sizes = ['B', 'KB', 'MB', 'GB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
}

示例输出

  • 10241 KB
  • 10485761 MB
  • 10737418241 GB
日期格式化
javascript 复制代码
function formatDate(date) {
  return new Date(date).toLocaleString('zh-CN', {
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit'
  });
}

示例输出2025/11/10 14:30:25


UI 设计与样式

HTML 结构

html 复制代码
<div class="file-selector">
    <h3>📁 文件选择</h3>
    <button id="select-file-btn">选择文件</button>
    <div class="file-info" id="file-info">
        <p><strong>已选择文件:</strong></p>
        <p class="file-path" id="file-path"></p>
        <p><strong>文件大小:</strong><span id="file-size"></span></p>
        <p><strong>文件类型:</strong><span id="file-type"></span></p>
        <p><strong>最后修改:</strong><span id="file-modified"></span></p>
    </div>
</div>

CSS 样式

css 复制代码
.file-selector {
    margin-top: 30px;
    padding: 20px;
    background: rgba(255, 255, 255, 0.15);
    border-radius: 10px;
}

.file-selector button {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    border: none;
    padding: 12px 30px;
    font-size: 1em;
    border-radius: 25px;
    cursor: pointer;
    transition: all 0.3s ease;
    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}

.file-selector button:hover {
    transform: translateY(-2px);
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
}

.file-info {
    margin-top: 20px;
    padding: 15px;
    background: rgba(255, 255, 255, 0.1);
    border-radius: 8px;
    text-align: left;
    display: none;
}

.file-info.show {
    display: block;
    animation: fadeInUp 0.5s ease;
}

.file-path {
    word-break: break-all;
    font-family: 'Monaco', 'Courier New', monospace;
    background: rgba(0, 0, 0, 0.2);
    padding: 8px;
    border-radius: 4px;
}

设计要点

  1. 响应式交互:按钮悬停效果,提升用户体验
  2. 信息展示:文件信息区域初始隐藏,选择文件后显示
  3. 路径显示:使用等宽字体,支持长路径自动换行
  4. 动画效果 :使用 fadeInUp 动画,提升视觉体验

完整代码示例

main.js

javascript 复制代码
const { app, BrowserWindow, ipcMain, dialog } = require('electron')
const path = require('path')

let mainWindow = null

function createWindow () {
  // 创建浏览器窗口
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false
    }
  })

  // 加载 index.html
  mainWindow.loadFile('index.html')
}

// 处理文件选择请求
ipcMain.handle('select-file', async () => {
  const window = BrowserWindow.getFocusedWindow() || mainWindow
  const result = await dialog.showOpenDialog(window, {
    properties: ['openFile'],
    filters: [
      { name: '所有文件', extensions: ['*'] },
      { name: '文本文件', extensions: ['txt', 'md', 'json'] },
      { name: '图片文件', extensions: ['jpg', 'jpeg', 'png', 'gif', 'bmp'] },
      { name: '视频文件', extensions: ['mp4', 'avi', 'mov', 'mkv'] }
    ]
  })
  
  if (!result.canceled && result.filePaths.length > 0) {
    return {
      success: true,
      filePath: result.filePaths[0]
    }
  }
  
  return {
    success: false,
    filePath: null
  }
})

// 当 Electron 完成初始化时创建窗口
app.whenReady().then(() => {
  createWindow()

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

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

index.html(关键部分)

html 复制代码
<div class="file-selector">
    <h3>📁 文件选择</h3>
    <button id="select-file-btn">选择文件</button>
    <div class="file-info" id="file-info">
        <p><strong>已选择文件:</strong></p>
        <p class="file-path" id="file-path"></p>
        <p><strong>文件大小:</strong><span id="file-size"></span></p>
        <p><strong>文件类型:</strong><span id="file-type"></span></p>
        <p><strong>最后修改:</strong><span id="file-modified"></span></p>
    </div>
</div>

<script>
const fs = require('fs');
const path = require('path');
const { ipcRenderer } = require('electron');

const selectFileBtn = document.getElementById('select-file-btn');
const fileInfo = document.getElementById('file-info');
const filePathEl = document.getElementById('file-path');
const fileSizeEl = document.getElementById('file-size');
const fileTypeEl = document.getElementById('file-type');
const fileModifiedEl = document.getElementById('file-modified');

// 格式化文件大小
function formatFileSize(bytes) {
    if (bytes === 0) return '0 B';
    const k = 1024;
    const sizes = ['B', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
}

// 格式化日期
function formatDate(date) {
    return new Date(date).toLocaleString('zh-CN', {
        year: 'numeric',
        month: '2-digit',
        day: '2-digit',
        hour: '2-digit',
        minute: '2-digit',
        second: '2-digit'
    });
}

// 获取文件信息
function getFileInfo(filePath) {
    try {
        const stats = fs.statSync(filePath);
        const ext = path.extname(filePath).toLowerCase();
        
        return {
            path: filePath,
            size: stats.size,
            type: ext || '未知类型',
            modified: stats.mtime
        };
    } catch (error) {
        console.error('获取文件信息失败:', error);
        return null;
    }
}

// 处理文件选择
selectFileBtn.addEventListener('click', async () => {
    try {
        const result = await ipcRenderer.invoke('select-file');
        
        if (result.success && result.filePath) {
            const fileInfoData = getFileInfo(result.filePath);
            
            if (fileInfoData) {
                filePathEl.textContent = fileInfoData.path;
                fileSizeEl.textContent = formatFileSize(fileInfoData.size);
                fileTypeEl.textContent = fileInfoData.type;
                fileModifiedEl.textContent = formatDate(fileInfoData.modified);
                fileInfo.classList.add('show');
            } else {
                alert('无法读取文件信息');
            }
        } else {
            console.log('用户取消了文件选择');
        }
    } catch (error) {
        console.error('选择文件时出错:', error);
        alert('选择文件时出错: ' + error.message);
    }
});
</script>

功能扩展

1. 多文件选择

修改主进程代码:

javascript 复制代码
ipcMain.handle('select-files', async () => {
  const window = BrowserWindow.getFocusedWindow() || mainWindow
  const result = await dialog.showOpenDialog(window, {
    properties: ['openFile', 'multiSelections'],  // 允许多选
    filters: [
      { name: '所有文件', extensions: ['*'] }
    ]
  })
  
  if (!result.canceled && result.filePaths.length > 0) {
    return {
      success: true,
      filePaths: result.filePaths  // 返回多个路径
    }
  }
  
  return {
    success: false,
    filePaths: []
  }
})

2. 目录选择

javascript 复制代码
ipcMain.handle('select-directory', async () => {
  const window = BrowserWindow.getFocusedWindow() || mainWindow
  const result = await dialog.showOpenDialog(window, {
    properties: ['openDirectory']  // 选择目录
  })
  
  if (!result.canceled && result.filePaths.length > 0) {
    return {
      success: true,
      directoryPath: result.filePaths[0]
    }
  }
  
  return {
    success: false,
    directoryPath: null
  }
})

3. 保存文件对话框

javascript 复制代码
ipcMain.handle('save-file', async (event, defaultPath, defaultFilename) => {
  const window = BrowserWindow.getFocusedWindow() || mainWindow
  const result = await dialog.showSaveDialog(window, {
    defaultPath: defaultPath || app.getPath('documents'),
    defaultFilename: defaultFilename || 'untitled.txt',
    filters: [
      { name: '文本文件', extensions: ['txt'] },
      { name: '所有文件', extensions: ['*'] }
    ]
  })
  
  if (!result.canceled && result.filePath) {
    return {
      success: true,
      filePath: result.filePath
    }
  }
  
  return {
    success: false,
    filePath: null
  }
})

4. 读取文件内容

javascript 复制代码
// 在主进程中添加
ipcMain.handle('read-file', async (event, filePath) => {
  try {
    const fs = require('fs').promises;
    const content = await fs.readFile(filePath, 'utf-8');
    return {
      success: true,
      content: content
    };
  } catch (error) {
    return {
      success: false,
      error: error.message
    };
  }
})

// 在渲染进程中调用
const result = await ipcRenderer.invoke('read-file', filePath);
if (result.success) {
  console.log('文件内容:', result.content);
}

5. 文件预览(图片)

javascript 复制代码
function displayImagePreview(filePath) {
  const img = document.createElement('img');
  img.src = `file://${filePath}`;
  img.style.maxWidth = '100%';
  img.style.maxHeight = '400px';
  img.style.borderRadius = '8px';
  
  const previewDiv = document.getElementById('image-preview');
  previewDiv.innerHTML = '';
  previewDiv.appendChild(img);
}

最佳实践

1. 错误处理

javascript 复制代码
selectFileBtn.addEventListener('click', async () => {
  try {
    // 禁用按钮,防止重复点击
    selectFileBtn.disabled = true;
    selectFileBtn.textContent = '选择中...';
    
    const result = await ipcRenderer.invoke('select-file');
    
    if (result.success && result.filePath) {
      // 处理文件
      const fileInfoData = getFileInfo(result.filePath);
      if (fileInfoData) {
        displayFileInfo(fileInfoData);
      } else {
        showError('无法读取文件信息');
      }
    }
  } catch (error) {
    console.error('选择文件时出错:', error);
    showError('选择文件时出错: ' + error.message);
  } finally {
    // 恢复按钮状态
    selectFileBtn.disabled = false;
    selectFileBtn.textContent = '选择文件';
  }
});

2. 文件大小限制

javascript 复制代码
function getFileInfo(filePath) {
  try {
    const stats = fs.statSync(filePath);
    const maxSize = 100 * 1024 * 1024; // 100MB
    
    if (stats.size > maxSize) {
      return {
        error: '文件过大,最大支持 100MB'
      };
    }
    
    return {
      path: filePath,
      size: stats.size,
      // ...
    };
  } catch (error) {
    return { error: error.message };
  }
}

3. 文件类型验证

javascript 复制代码
function validateFileType(filePath, allowedTypes) {
  const ext = path.extname(filePath).toLowerCase().slice(1);
  return allowedTypes.includes(ext);
}

// 使用
if (!validateFileType(filePath, ['jpg', 'png', 'gif'])) {
  alert('不支持的文件类型,请选择图片文件');
  return;
}

4. 路径安全处理

javascript 复制代码
const path = require('path');

function sanitizePath(filePath) {
  // 规范化路径,防止路径遍历攻击
  return path.normalize(filePath);
}

// 检查路径是否在允许的目录内
function isPathAllowed(filePath, allowedDir) {
  const normalized = path.normalize(filePath);
  const allowed = path.normalize(allowedDir);
  return normalized.startsWith(allowed);
}

5. 异步文件读取

javascript 复制代码
// 使用 Promise 版本的 fs
const fs = require('fs').promises;

async function getFileInfoAsync(filePath) {
  try {
    const stats = await fs.stat(filePath);
    const ext = path.extname(filePath).toLowerCase();
    
    return {
      path: filePath,
      size: stats.size,
      type: ext || '未知类型',
      modified: stats.mtime
    };
  } catch (error) {
    console.error('获取文件信息失败:', error);
    return null;
  }
}

6. IPC 处理器位置

推荐做法 :将 IPC 处理器放在 app.whenReady() 外部,避免重复注册。

javascript 复制代码
// ✅ 推荐:全局注册
ipcMain.handle('select-file', async () => {
  // ...
});

app.whenReady().then(() => {
  createWindow();
});

// ❌ 不推荐:在 createWindow 内部注册
function createWindow() {
  // ...
  ipcMain.handle('select-file', async () => {
    // 每次创建窗口都会注册,可能导致问题
  });
}

常见问题

1. electron: command not found

问题 :运行 npm start 时提示找不到 electron 命令。

解决方案

bash 复制代码
# 重新安装依赖
npm install

# 或使用 npx
npx electron .

2. IPC 通信失败

问题ipcRenderer.invoke() 返回 undefined 或报错。

可能原因

  • IPC 处理器未正确注册
  • 处理器名称不匹配
  • 主进程未启动

解决方案

javascript 复制代码
// 检查处理器是否注册
console.log('IPC handlers:', ipcMain.eventNames());

// 确保在 app.whenReady() 之前注册
ipcMain.handle('select-file', async () => {
  // ...
});

3. 文件路径问题

问题:在不同操作系统上路径格式不同。

解决方案

javascript 复制代码
const path = require('path');

// 使用 path 模块处理路径
const normalizedPath = path.normalize(filePath);
const dir = path.dirname(filePath);
const ext = path.extname(filePath);
const basename = path.basename(filePath);

4. 大文件读取卡顿

问题:读取大文件时界面卡顿。

解决方案

javascript 复制代码
// 使用流式读取
const fs = require('fs');
const readStream = fs.createReadStream(filePath, 'utf-8');

readStream.on('data', (chunk) => {
  // 处理数据块
});

readStream.on('end', () => {
  // 读取完成
});

5. 对话框不显示

问题dialog.showOpenDialog() 不显示对话框。

可能原因

  • 窗口对象为 null
  • 窗口未聚焦

解决方案

javascript 复制代码
// 使用 getFocusedWindow() 或保存窗口引用
const window = BrowserWindow.getFocusedWindow() || mainWindow;

if (!window) {
  console.error('没有可用的窗口');
  return;
}

const result = await dialog.showOpenDialog(window, {
  // ...
});

总结

通过本文,我们学习了:

  1. IPC 通信 :使用 ipcRenderer.invoke()ipcMain.handle() 实现进程间通信
  2. 对话框 API :使用 dialog.showOpenDialog() 打开系统文件选择对话框
  3. 文件系统操作 :使用 Node.js fs 模块读取文件信息
  4. UI 实现:创建美观的文件选择界面
  5. 错误处理:完善的错误处理和用户提示
  6. 功能扩展:多文件选择、目录选择、保存文件等扩展功能

关键要点

  • IPC 通信是 Electron 中渲染进程与主进程交互的核心机制
  • 对话框 API提供了跨平台的原生文件选择体验
  • 错误处理对于提升用户体验至关重要
  • 文件验证可以防止安全问题和错误操作

下一步学习


祝您开发愉快! 🚀


最后更新:2025年11月10日
Electron 版本:39.1.1
Node.js 版本:20.17.0

相关推荐
www_stdio2 小时前
使用 Ajax 实现异步数据请求:从原理到实践
javascript·ajax·html
却尘2 小时前
一个"New Chat"按钮,为什么要重构整个架构?
前端·javascript·next.js
庙堂龙吟奈我何3 小时前
js中哪些数据在栈上,哪些数据在堆上?
开发语言·javascript·ecmascript
景早3 小时前
商品案例-组件封装(vue)
前端·javascript·vue.js
用户6600676685395 小时前
深入解析JavaScript数组:从内存原理到高效遍历实践
前端·javascript
qiao若huan喜5 小时前
9、webgl 基本概念 + 复合变换 + 平面内容复习
前端·javascript·信息可视化·webgl
金鸿客5 小时前
鸿蒙相对布局RelativeContainer详解
harmonyos