从投标客户端看 Electron + React 工程化实践

从投标客户端看 Electron + React 工程化实践

引言

  • Electron + React ,桌面端跨平台的组合架构。

  • 结合真实项目(投标客户端),从工程化角度聊聊落地。

一、项目架构总览

  • 整体目录结构

    客户端整体目录分为工程化、静态资源、业务代码,三个层面。

    首先,工程化目录主要包含整个客户端的构建逻辑与资源,如:webpack基础配置、构建脚本、构建资源。

    markdown 复制代码
    .erb/
    ├── configs/          # webpack 主进程 / 渲染进程 / preload / DLL 等配置
    ├── dll/              # 开发态 DLL 构建产物
    ├── img/              # ERB 相关图片
    ├── mocks/            # Jest 等 mock
    └── scripts/          # 清理、端口检测、notarize、electron-rebuild...
        └── package/      # npm run package 打包

    其次,静态资源目录里的资源属于业务级,只存放有业务代码所需要使用的资源。如:三方插件、子进程脚本。

    markdown 复制代码
    static/ 静态资源和子进程
    ├── childProcess/     # Node 子进程脚本(zip/unzip、磁盘信息等),自带依赖
    ├── generic/          # PDF.js 相关(build/、web/)
    ├── viewer.pdf / viewer.scan.pdf 等
    └── README.md

    最后,业务级代码统一存放在src目录下,具体如下。

    markdown 复制代码
    src/
    ├── clientLog/        # 客户端日志封装
    ├── constants/        # 全局常量(含 IPC 键名、窗口尺寸等)
    ├── main/             # Electron 主进程
    │   ├── index.ts      # 应用入口(单例锁、协议、快捷键、创建主窗口等)
    │   ├── preload.ts    # Preload(contextBridge 暴露 electron API)
    │   ├── mainBrowserWindow/   # 主窗口创建、Koa 服务、加载 URL、注册 IPC
    │   ├── server/       # Koa 本地静态服务
    │   ├── pathFinder/   # 解析 preload / 静态目录 / HTML 路径
    │   ├── ipc/          # ipcMain 模块化 handlers(browser、dialog、file、zip...)
    │   ├── ipcRenderHelper/     # 供 preload 使用的 ipc 封装
    │   ├── ipcUpdate/    # 自动更新相关 IPC
    │   ├── database/     # SQLite 等主进程侧存储
    │   ├── logger.ts, getProcess.ts, createChildProcess.ts, ...
    │   └── config/       # 如安装路径等
    ├── renderer/         # React 渲染进程
    │   ├── index.tsx     # 渲染入口
    │   ├── App.tsx
    │   ├── index.ejs     # HTML 模板
    │   ├── router/       # React Router
    │   ├── store/        # Redux(RTK)各 slice
    │   ├── api/          # 与后端/本地 DB 等接口层
    │   ├── common/       # hooks、constants、util、类型等共享逻辑
    │   ├── components/   # 通用 UI 组件
    │   ├── modules/      # 业务模块(Header、导入工程、树形侧边栏等)
    │   ├── layouts/      # 布局(Default、Process 流程布局等)
    │   ├── pages/        # 页面级(Login、ProjectList、ProjectInfo、制标、验标、PDF 导入等)
    │   ├── style/        # 全局与主题 less
    │   └── resource/     # 如内嵌 js 资源
    ├── util/             # 与 renderer/main 弱耦合的小工具
    ├── global.d.ts
    └── window.d.ts       # window 上挂载类型(如 electron)
  • 核心入口文件梳理

    先说明,Electron 应用不像单页 Web 项目那样只有一个 index.html 入口,而是至少经历 主进程启动 →本地服务 → 加载页面 → Preload 注入 → 渲染进程挂载 React 这一条链。链条上的每一步都由少量「核心文件」承担:有的负责进程与窗口生命周期,有的负责把系统能力以受控方式暴露给页面,有的则负责前端路由与业务层。下表按 从声明到构建 的顺序,记录这些文件与各自职责,便于快速建立全局心智。

    层级 文件(路径) 作用
    启动声明 package.json 的 main 指定 Electron 加载的主进程入口模块。
    主进程 · 进程 src/main/index.ts 应用级生命周期:单例锁、协议、就绪后触发建窗等。
    主进程 · 应用窗口配置 src/main/mainBrowserWindow/index.ts 窗口级:创建 BrowserWindow、起 Koa 本地服务、loadURL、注册 IPC。
    主进程 · 路径 src/main/pathFinder/index.ts 统一解析 Preload 路径、页面 URL(开发 Dev Server / 生产本地 HTML)、静态目录等。
    主进程 · 服务 src/main/server/index.ts 本地 HTTP 静态服务(供生产态加载 index.html 与资源)。
    主进程 · 通信 src/main/ipc/index.ts 及同目录各模块 ipcMain 能力按模块拆分注册(文件、对话框、下载等)
    桥接 src/main/preload.ts Preload:contextBridge 暴露 window.electron(IPC 封装、日志、fs 等)。
    桥接 · IPC 封装 src/main/ipcRenderHelper/index.ts 渲染侧 IPC 白名单与 on/once 封装,避免裸传 ipcRenderer。
    渲染进程 · 前端入口 src/renderer/index.tsx 全局初始化(日志、CA 等)后 ReactDOM.render,挂 Provider + HashRouter。
    渲染进程 · 应用 src/renderer/App.tsx 路由之上的全局层:环境扫描、更新、与主进程协同的窗口/路由副作用等。
    渲染进程 · 路由 src/renderer/router/index.tsx useRoutes 路由表:登录、项目列表、流程各页。
    主进程构建 .erb/configs/webpack.config.main.prod.ts 生产打 主进程 + preload 双入口(main.js / preload.js)。
    渲染进程构建 .erb/configs/webpack.config.renderer.*.ts 开发/生产打 渲染进程,入口为 src/renderer/index.tsx。
  • 多进程模型:主进程 + 渲染进程 + Preload 脚本

读者需明确electron应用里同时存在主进程和渲染进程,如果没有沙箱机制,会导致两个进程的上下文混乱引发各种问题,同时主进程和渲染进程也是无法直接通信的,接下来用以下两点进行讲述。

  1. 什么是沙箱机制,它有什么作用?

    沙箱(Sandbox):从安全的角度来说,在计算机里,指的是把某段程序关在「权限受限的运行环境」里跑,让它拿不到或很难拿到整套系统能力,从而降低「一段网页脚本、一个插件崩溃或被利用」时对整台电脑的伤害。

    在 Chromium / Electron 里,和前端同学最相关的是 渲染进程沙箱:

    打开页面时,HTML/CSS/JS 主要在 渲染进程里执行,这个进程在操作系统层面往往被加上各种限制(不能想读哪就读哪、不能随意启动任意程序等,具体能力随版本与配置而变)。 真正需要 读用户文件、弹系统对话框、深度集成系统 的事,通常交给 主进程或经主进程授权后再做。 这样做的直接好处是:界面再复杂,默认也有一层「隔离带」,恶意或出错的页面代码更难一步跳到「整盘操作系统权限」。

    Electron 里还要叠一层「上下文」概念:页面里的 JavaScript 和 Preload 脚本并不混在同一个全局环境里;Preload 可以通过 contextBridge 有选择地把少量 API 挂到页面上。你可以把沙箱想成「围墙」,把 Preload 想成围墙上 登记过的门------只放行约定好的能力(例如封装好的 IPC),而不是把 Node、文件系统整包塞进页面。

  2. 在客户端里沙箱机制是如何应用的?

electron里沙箱机制被称为沙盒化行为,这是electron里可以进行配置的,包括指定沙盒可进行的行为权限,从electron 20 版本开始,默认就启用了沙盒,当然也可被配置禁用沙盒,但对于相关禁用沙盒的指令行为,官方文档里明确提及,"我们强烈建议你只针对测试用途开启此标志,并且 永远 不要用于生产环境。"具体想了解electron的沙盒化行为,详细了解可阅读进程沙盒化

那么,在客户端里,分为主进程、渲染进程,它们之间如何进行通信,如下图所示。

二、多窗口架构设计

  • 主窗口与子窗口的创建管理

    设计思路:一类窗口,两种用法

    统一窗口壳

    主窗口和子窗口共用同一套窗口封装(同一类「浏览器窗口」配置),避免两套 Preload、两套 IPC 约定。差别主要在:有没有父窗口、首次打开的地址是否立刻加载、初始尺寸是否一致。

    主窗口

    应用启动后创建第一个窗口,没有父窗口。创建顺序为:建好窗口 → 为页面准备访问方式→ 加载入口页(登录页) → 再挂上主窗口要用的系统能力(文件、下载、窗口状态等)。窗口关掉时,要顺带释放只为页面服务的那部分资源(例如本地 HTTP),避免残留。

    子窗口

    由已有窗口在逻辑上发起:传入父窗口,让系统在窗口层级上建立父子关系。表现层上父子窗口可达到同时存在,互不干扰。

  • 投标场景下哪些功能走了子窗口?

在投标场景下,子窗口,专门用来「只读看另一个项目」。子窗口解决的是 同时两件事:

主窗口:继续编制新项目标书(可写、可走流程)。

子窗口:只读打开别的项目(常见是旧项目)的标书,用于对照、核对、摘录,而不打断当前编制上下文。也就是说,在投标场景下,子窗口不是「流程里下一步换一块屏」,而是 「边写新的,边打开旧的(只读)」 的并行布局;还可以再理解成:多个只读视窗对应多份历史材料,主窗口始终守住「当前这份正在编的活」。

三、IPC 通信模式

  • 模块化的 IPC Handlers

    本工程里通过模块化,来管理 IPC Handlers,按照以下概念进行划分,按领域分文件、统一出口再注册。

    之所以这么处理,是因为主进程里的 IPC 很容易长成「一个文件几千行」:文件对话框、下载、解压、环境检测、窗口尺寸...全堆在一起。结果是:改一处怕牵一片、Code Review 难拆、多人协作总冲突。

    模块化就是把主进程能力按业务域拆开:每个文件只负责一类事(例如「保存到本地」「打开文件」「浏览器窗口行为」),对外只暴露一段注册逻辑------在窗口就绪时,把 ipcMain 的监听或 invoke 处理绑好。只要记住模型:「许多小模块 + 一个总装配点」

  • Preload 脚本与 Context Bridge

    **Preload(预加载脚本):**渲染进程里的页面(React 业务代码)和 Node / 系统能力之间,不能指望「像写服务端一样随便 require」。Electron 的做法是:在页面真正执行之前,先跑一段 Preload 脚本。它夹在主进程和页面脚本之间:负责把「允许页面使用的极少数能力」整理好,再交给页面用,以此把控安全。

    Context Bridge (上下文隔离):

    开启上下文隔离时,Preload 里的 JavaScript 和页面里的 JavaScript 不共用同一个全局环境,页面无法直接访问到 Preload 里的变量。

    Context Bridge 的作用就是:在隔离的前提下,显式指定哪些东西可以挂到页面的 window 上,变成页面能访问的稳定、可预期的 API。用官方文档里的具体描述【在渲染进程中,预加载脚本暴露给已加载的页面 API 是一个常见的使用方式。 当上下文隔离时,您的预加载脚本可能会暴露一个常见的全局window对象给渲染进程。 此后,您可以从中添加任意的属性到预加载脚本】。通过上下文隔离,可以解决这个问题,详见上下文隔离

    对读者来说,只需明白三句话:

    隔离:页面和 Preload 各住一间屋,减少页面脚本「顺手牵走」过多能力。

    登记:只有通过 Context Bridge 登记过的属性/方法,才会出现在页面上。

    契约:页面侧只认 window 上这一小块 API,相当于前后端之间的「客户端 SDK」。

  • 业务模块与主进程通信

等同于渲染进程如何与主进程进行通信,结合上文介绍,渲染进程和主进程之间还有一层Preload和Context Bridge,他们之间关系如下:

四、React 与 Electron 的融合

  • Webpack 多入口打包

    首先说明,为什么存在多入口打包。对于普通前端项目往往只有一个浏览器端入口;Electron 里却至少存在三种要被打成 JavaScript 的角色:

    主进程:Node 侧、管窗口和系统能力,构建目标一般是 electron-main。

    Preload:在页面加载前注入,构建目标通常与主进程产物同族。

    渲染进程:本工程中是React 应用,构建目标是带 DOM 的 electron-renderer / web,走 Dev Server 或打进静态资源。

    三份代码跑在 不同上下文,依赖和环境变量也不完全一样,因此用 多个 Webpack 入口(多份配置或多 entry) 分别打包,比硬塞进一个 entry 更符合 Electron 的边界。因此,在投标客户端里,有以下的打包配置,当然实际情况中,还有其他的模块需要打包,这里,仅供参考。

    文件 作用
    webpack.config.base.ts 各环境共用的 基础配置(如 externals、与 release/app/package.json 依赖对齐等),被其它 config merge 使用。
    webpack.paths.ts 路径与别名:src/main、src/renderer、dll、主题 less、以及 @common、@pages、@store 等 alias,供所有 webpack 配置引用。
    webpack.env.config.ts 构建期环境/主题:读取根目录 environment.js 的 getEnvConfig(),默认主题 theme(如 lan),供路径与 less 变量等使用。
    webpack.config.main.prod.ts 生产主进程:target: electron-main,双入口 main(src/main/index.ts)+ preload(src/main/preload.ts),产出主进程与 preload 的 JS。
    webpack.config.preload.dev.ts 开发 Preload:单独把 preload.ts 编到 .erb/dll/preload.js,对应脚本 npm run start:preload。
    webpack.config.renderer.dev.ts 开发渲染进程:webpack-dev-server + HMR 等,入口含 src/renderer/index.tsx,对应 npm run start / start:renderer。
    webpack.config.renderer.prod.ts 生产渲染进程:打包 React 页面资源,入口 src/renderer/index.tsx,对应 npm run build:renderer。
    webpack.config.renderer.dev.dll.ts 开发 DLL:把部分依赖打成 DLL,加速开发构建;在 postinstall 里执行。
    webpack.config.eslint.ts ESLint 解析用:直接 require renderer dev 配置,让 eslint-import-resolver-webpack 与开发态别名一致。
  • React + Redux + Router 的集成

    这三件套集成都跑在 渲染进程 里,由 渲染侧的主入口打进同一个前端包。它们不和主进程、Preload 混编,只是通过 IPC / window.electron 与桌面能力协作。也就是说:Electron 只提供壳与系统能力,应用状态与路由仍是典型 React 工程。也意味着该部分是可以自定义的,不一定要采用React + Redux +Router来构建前端应用。

  • Koa 本地开发服务器

主进程里起了一个基于 Koa 的轻量 HTTP 服务,配合 loadURL 用 http://localhost:端口/... 拉取页面和静态资源,行为更接近浏览器环境,也便于约定路径与静态资源根目录。服务挂了 koa-static 做静态文件。窗口关闭时对监听端口做优雅关停,避免残留占用。

生产环境下,主窗口加载的地址会落在本机 Koa 端口上,并指向打包后的 index.html 等资源,整页应用由这台本地服务统一提供。

开发环境下,日常界面入口在 Webpack Dev Server 上跑热更新,端口与 Koa 不是同一个;Koa 仍会随窗口创建而启动,用于提供静态根路径,并通过 IPC 把 http://localhost:<Koa端口> 交给渲染层。PDF 预览、扫描预览、电子签章等需要以URL形式读取文件的功能,会依赖这个基地址,而不是在业务里写死端口。

Koa 只承担「在本机起一个稳定的静态与地址出口」,业务状态与路由仍在 React 侧;主进程保持服务层尽量薄,复杂业务不往中间件里堆,以免主进程膨胀、也难维护。

与 开发服务器相比:Webpack 服务给前端开发用,Koa 是装进客户端里的本地小站,二者分工不同。

总而言之,Koa就是一个部署在用户端环境下的小型服务器,用来搭载客户端的进程。

五、项目打包

  • electron-builder

electron-builder,实际上由一个个构建脚本组成。在本项目中 以 npm run package 启动,按照流水线一样的工作机制依次执行构建脚本,具体流程如下。

六、结语

读者可以把 Electron 桌面应用想成几层叠在一起的系统:主进程负责窗口与系统能力,Preload 负责把能力以受控方式交给页面,渲染进程里再用熟悉的 前端技术栈做界面与状态。工程上是否省心,往往取决于这些边界是否干净、通信是否可追踪、打包是否一条命令能复现。本文以投标客户端为例,串起了多窗口、IPC、Webpack 多入口、本地 HTTP 服务以及从构建一条流水线,当然,这并不是唯一标准答案。最后,希望对于想要构建electron的读者们,有所帮助。

相关推荐
得想办法娶到那个女人3 小时前
Vite + Vue 项目打包为 Electron 桌面应用 完整指南
前端·vue.js·electron
李李李勃谦4 小时前
Vue3 + Electron + OpenHarmony 跨平台实战:从架构设计到 Markdown 编辑器完整实现
javascript·华为·electron·编辑器·harmonyos
森叶4 小时前
Electron 实战:utilityProcess 服务脚本热更新、用户目录优先启动与 asar 依赖解析
前端·javascript·electron
森叶1 天前
Electron 实战:用 utilityProcess 开子进程,去端口化承载协议处理,并由主进程拦截渲染请求后统一中转
前端·javascript·electron
茅盾体1 天前
Electron图标相关
java·前端·electron
ejinxian2 天前
Rust GUI框架Azul与Electron、WebView2
前端·javascript·electron
孙凯亮3 天前
Electron 项目终极实战总结:从黑屏踩坑到自动更新全流程
前端·electron
Python私教4 天前
FuturesDesk:配置驱动 UI 的 Electron 金融桌面应用模板
ui·金融·electron
孙凯亮4 天前
Electron 接口请求全解析:从疑问到落地(真实开发对话整理)
前端·electron