文件选择能力集成与鸿蒙适配详解(Electron × 鸿蒙容器)

本文详细介绍如何在当前项目的 web_engine 模块中,为 Electron 页面在鸿蒙容器环境下增加“选择文件”的能力,并给出在接口不可用时的浏览器级回退方案。文章围绕安全、可移植、可适配的实现方式展开,包含架构设计、关键代码、行为表现、鸿蒙平台适配要点以及常见问题排查建议。


背景与目标

  • 项目路径:ohos_hap/web_engine/src/main/resources/resfile/resources/app/
  • 相关文件:
    • main.js(主进程)
    • preload.js(预加载脚本,隔离上下文安全桥)
    • index.html(渲染层页面)

目标是在保持 webPreferences 的安全设置(nodeIntegration: falsecontextIsolation: true)的前提下,实现文件选择对话框的能力,适用于 Electron 在鸿蒙容器中的运行场景。同时,当容器未适配或 IPC 未就绪导致接口不可用时,提供 <input type="file"> 的回退方式,以保证基本可用性。


架构设计

  • 主进程(main.js):注册 IPC 接口,调用 dialog.showOpenDialog 打开系统文件选择对话框,并将选择结果返回给渲染层。
  • 预加载脚本(preload.js):通过 contextBridge.exposeInMainWorld 暴露 selectFiles(options) 安全 API,渲染层使用该 API 发起调用。
  • 渲染层页面(index.html):提供“选择文件”按钮,调用预加载 API;当接口不可用时自动回退到隐藏的 <input type="file">

示意:

[Renderer (index.html)] --invoke--> [Preload (contextBridge)] --ipc--> [Main (ipcMain + dialog)]
                         \--fallback: <input type="file"> (no IPC)

关键实现代码

1)主进程:注册文件选择 IPC 接口(app/main.js

// 主进程:增加文件选择能力,适配 Electron × 鸿蒙
// 说明:通过 IPC 暴露文件选择对话框,渲染层在 contextIsolation 下安全使用。
const { app, BrowserWindow, Tray, nativeImage, Menu, ipcMain, dialog } = require('electron');
const path = require('path');

let mainWindow, tray;

function createWindow() {
  // ... 省略窗口与托盘创建代码 ...
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: false,
      contextIsolation: true,
      preload: path.join(__dirname, 'preload.js'),
    },
  });

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

// 应用就绪后创建窗口,并注册文件选择 IPC 接口
app.whenReady()
  .then(createWindow)
  .then(() => {
    /**
     * 渲染层调用:选择文件对话框
     * @param {Electron.IpcMainInvokeEvent} _event - IPC 调用事件(未使用)
     * @param {Object} options - 对话框自定义参数(可选),与 Electron 的 showOpenDialog 选项一致
     * @returns {Promise<Electron.OpenDialogReturnValue>} 包含是否取消与选中文件路径的返回值
     * 说明:
     * - 默认允许选择多个文件(multiSelections),可通过 options.properties 覆盖
     * - 在鸿蒙容器中,底层 Electron 的对话框能力将由容器适配具体实现
     */
    ipcMain.handle('select-files', async (_event, options = {}) => {
      const defaultOptions = {
        title: '选择文件',
        properties: ['openFile', 'multiSelections'],
        // 可选:filters 例如只选择图片或文本
        // filters: [{ name: 'All Files', extensions: ['*'] }]
      };
      const merged = { ...defaultOptions, ...options };
      const result = await dialog.showOpenDialog(mainWindow ?? undefined, merged);
      return result; // { canceled: boolean, filePaths: string[] }
    });
  });

2)预加载脚本:暴露安全 API(app/preload.js

// 预加载脚本:在渲染层安全地暴露必要 API,并注入平台信息到页面
const { contextBridge, ipcRenderer } = require('electron');
const os = require('os');

contextBridge.exposeInMainWorld('electronAPI', {
  // 平台信息(已存在)
  platform: {
    processPlatform: process.platform,
    osPlatform: os.platform(),
    osType: os.type(),
  },

  /**
   * 选择文件:调用主进程对话框
   * @param {Object} options - 可选参数,与 Electron 的 showOpenDialog 选项一致
   * @returns {Promise<{canceled: boolean, filePaths: string[]}>}
   * 说明:由于启用了 contextIsolation,这里通过 IPC 安全调用主进程。
   */
  selectFiles: (options) => ipcRenderer.invoke('select-files', options),
});

3)渲染层页面:交互与回退(app/index.html

<!-- 选择文件 UI:按钮 + 状态 + 列表(含浏览器回退 input) -->
<button id="btn-pick">选择文件</button>
<span id="pick-status">未选择</span>
<input id="file-input" type="file" multiple style="display:none" />
<ul id="file-list"></ul>

<script>
  document.addEventListener('DOMContentLoaded', () => {
    const btnPick = document.getElementById('btn-pick');
    const statusEl = document.getElementById('pick-status');
    const listEl = document.getElementById('file-list');
    const fileInput = document.getElementById('file-input');

    const renderList = (paths) => {
      listEl.innerHTML = '';
      (paths || []).forEach((p) => {
        const li = document.createElement('li');
        li.textContent = p; // 标准模式显示绝对路径;回退模式显示文件名
        listEl.appendChild(li);
      });
    };

    // 浏览器回退:监听文件选择变更,展示文件名(不含绝对路径)
    fileInput?.addEventListener('change', () => {
      const files = Array.from(fileInput.files || []);
      if (files.length === 0) {
        statusEl.textContent = '已取消';
        renderList([]);
      } else {
        statusEl.textContent = `已选择 ${files.length} 个文件(浏览器回退)`;
        renderList(files.map(f => f.name));
      }
    });

    btnPick?.addEventListener('click', async () => {
      try {
        statusEl.textContent = '打开对话框中...';
        const hasElectronPicker = !!(window.electronAPI && window.electronAPI.selectFiles);
        if (!hasElectronPicker) {
          statusEl.textContent = '接口不可用,使用浏览器文件选择';
          fileInput?.click();
          return;
        }
        const result = await window.electronAPI.selectFiles({ properties: ['openFile', 'multiSelections'] });
        if (result.canceled) {
          statusEl.textContent = '已取消';
          renderList([]);
        } else {
          statusEl.textContent = `已选择 ${result.filePaths.length} 个文件`;
          renderList(result.filePaths);
        }
      } catch (err) {
        console.error('选择文件失败:', err);
        statusEl.textContent = '选择文件失败';
      }
    });
  });
</script>

行为表现与差异

  • 标准模式:存在 electronAPI.selectFiles 时,点击按钮会唤起系统文件选择对话框(由 Electron 容器适配);返回绝对路径列表。
  • 回退模式:当接口不可用(未暴露、容器未适配、权限限制等),自动触发 <input type="file">;返回文件名,不含绝对路径,这是浏览器安全模型所致。

鸿蒙平台适配要点

  • 容器适配:在鸿蒙环境,Electron 的 dialog.showOpenDialog 需由容器适配到系统文件选择能力。如果容器暂未开放或权限受限,会触发本文的回退方案。
  • 权限策略:若后续需要读取文件内容或访问路径,请按鸿蒙平台存储访问权限策略配置对应权限(例如媒体、文档访问)。
  • 预加载路径:确保 main.js 中的 preload: path.join(__dirname, 'preload.js') 指向部署后的真实路径,否则渲染层无法获得 electronAPI
  • UI 文案:页面已标注“浏览器回退”状态便于识别;如需统一体验,可隐藏该提示并在容器侧完善适配。

安全与工程实践

  • nodeIntegration: false + contextIsolation: true 是推荐安全配置,禁止渲染层直接使用 Node 能力,防止第三方页面/脚本越权访问。
  • 通过 contextBridge 暴露有限、白名单式 API(如 selectFiles),遵循“最小权限原则”。
  • 对返回数据进行最小依赖:仅消费 canceledfilePaths,避免渲染层耦合主进程实现细节。

常见问题与排查

  • “接口不可用”常见原因:

    • 预加载脚本未正确加载或路径错误(检查 webPreferences.preload)。
    • 容器侧未注册/未适配 dialog.showOpenDialog
    • IPC 通道名称不一致(本文使用 'select-files')。
  • 托盘图标异常:当前目录缺少 electron_white.png 可能导致托盘创建失败,可暂时移除托盘逻辑或补充图标文件,以免影响窗口生命周期事件执行。

  • 返回路径为空:当用户取消选择时,canceled === truefilePaths 为空,属正常表现;页面状态会显示“已取消”。


扩展方向

  • 目录选择:将 properties 改为 ['openDirectory'],即可选择目录。
  • 保存对话框:在主进程使用 dialog.showSaveDialog 实现保存路径选择。
  • 类型过滤:通过 filters 指定扩展名,例如只选择图片:
window.electronAPI.selectFiles({
  properties: ['openFile', 'multiSelections'],
  filters: [{ name: 'Images', extensions: ['png', 'jpg', 'jpeg'] }],
});
  • 读取文件内容:在渲染层仅持有路径,实际读取建议通过主进程或受控的预加载桥接(谨慎开放),以保持安全边界。

构建与运行建议(Harmony 环境)

  • 按仓库提供的打包/构建流程,将 web_engine 模块打入 HAP 包;确保资源路径与容器加载路径一致。
  • 在实际设备或容器中运行时,行为以系统能力适配为准;若无对话框能力,回退逻辑会生效。
  • 如需统一体验,建议在容器侧完善 dialog 能力适配,或提供 ArkUI 原生文件选择能力对接。

结语

本文方案在保证安全隔离的前提下,为 Electron × 鸿蒙容器环境提供了可用的“选择文件”能力,并在适配不完全时提供了浏览器级回退。结合 IPC、预加载桥接与 UI 交互,即可满足多数跨平台场景;后续可根据业务需要扩展到目录选择、保存对话框以及文件读取等能力。

更多推荐