基于 Next.js 构建 Electron 桌面应用:with-electron 官方示例完整指南
在本仓库的 examples/with-electron 示例中,你将看到一种“以最少的工程配置”把 Next.js 页面装进 Electron 桌面应用的成熟方案:开发时借助 Next.js 自带的 HTTP 服务器热重载与路由能力,生产构建时通过 output: 'export' 把整个渲染进程预渲染为纯静态 HTML/CSS/JS,再用 electron-builder 打包分发,全程无需手写 Webpack 配置。读完本文,你将掌握该示例的目录结构、双进程(主进程/渲染进程)职责划分、开发与打包脚本的完整用法,以及如何通过 preload.js + contextBridge 安全地在 React 页面中调用 Electron 的 IPC 能力。
示例解决了什么问题
Electron 应用通常需要一个 HTML 入口和一个浏览器环境来加载它。如果直接用原生 HTML,页面路由、组件化与前端工程化都要自己搭建;如果引入 React,又需要额外配置 Babel、打包器等一整套工具链。而 with-electron 的思路是:把渲染层整体交给 Next.js,将 Next.js 应用作为一个独立的子目录(本示例中是 renderer/)来开发与构建,Electron 主进程只负责打开窗口和加载页面。
- 开发模式:运行一个 HTTP 服务器,由 Next.js 处理路由、热更新与服务端渲染,从而加快应用首屏渲染与开发迭代速度;
- 生产模式:不再依赖任何 Node.js 服务器,而是通过
output: 'export'把页面预生成为静态 HTML 文件,Electron 直接以file://协议加载这些产物。
这带来一个直接的好处:桌面应用的 UI 开发完全可以沿用 Web 开发者熟悉的 Next.js 工程化体验,最终又能以轻量的静态资源形态随 Electron 一起分发。
快速开始:用 create-next-app 引导项目
示例 README 提供了三种包管理器对应的引导命令,均通过 create-next-app 的 --example 参数从本仓库拉取模板:
npx create-next-app --example with-electron with-electron-app
yarn create next-app --example with-electron with-electron-app
pnpm create next-app --example with-electron with-electron-app
三条命令的效果完全一致:在 with-electron-app 目录下生成与 examples/with-electron 相同的工程骨架。安装依赖之后,创建生产应用直接执行:
npm run dist
该命令会依次完成「构建 Next.js 渲染产物」与「调用 electron-builder 产出可分发的安装包」两步。
工程结构剖析
示例目录采用了典型的主进程 / 渲染进程分离布局:
examples/with-electron
├── main/ # Electron 主进程
│ ├── index.js # 应用入口:创建窗口、加载页面、监听 IPC
│ └── preload.js # 预加载脚本:向渲染进程安全暴露 API
├── renderer/ # Next.js 应用(渲染进程)
│ ├── pages/
│ │ └── index.js # 唯一的页面
│ └── babel.config.js # 针对 Electron 目标的 Babel 配置
├── next.config.js # Next.js 配置(output: "export")
└── package.json # 脚本、依赖与 electron-builder 打包配置
关键设计在于 package.json 中的 "main": "main/index.js" —— Electron 启动时以 main/index.js 为进程入口,而 Next.js 应用则完整嵌套在 renderer 子目录中。
各 npm 脚本说明
package.json 中定义了五个脚本,构成了完整的开发与发布工作流:
| 脚本 | 实际命令 | 作用 |
|---|---|---|
clean |
rimraf dist renderer/.next renderer/out |
清理打包产物与 Next.js 的 .next、out 缓存目录 |
start |
electron . |
以当前目录(读取 main 字段指向的入口)启动 Electron,用于本地调试 |
build |
next build renderer |
仅构建渲染进程,产出 renderer/out 静态目录 |
pack-app |
npm run build && electron-builder --dir |
构建后生成未压缩的打包目录(便于快速验证,不生成安装器) |
dist |
npm run build && electron-builder |
构建后生成完整的可分发包(安装器/压缩包) |
依赖清单与 electron-builder 配置
依赖方面分为两组:运行期间由主进程直接 require 的包放在 dependencies(会被打进最终产物),仅构建期使用的放在 devDependencies:
dependencies:electron-is-dev(判断当前是否开发环境)与electron-next(负责引导 Next.js 的开发服务器 / 静态渲染);devDependencies:electron、electron-builder、next、react、react-dom。
electron-builder 的配置同样内联在 package.json 的 build 字段中:
"build": {
"asar": true,
"files": [
"main",
"renderer/out"
]
}
asar: true 表示把所有应用文件归档进 app.asar 以减少文件数量、加快加载;files 白名单只打包两个目录——Electron 主进程代码 main/ 与 Next.js 静态导出产物 renderer/out,从而保证安装包只包含必要内容,node_modules 中未被引用的依赖不会混入。
开发与生产两种加载路径
main/index.js 完整展示了两种模式的切换逻辑,这正是整个示例的核心:
const { join } = require("path");
const { format } = require("url");
const { BrowserWindow, app, ipcMain } = require("electron");
const isDev = require("electron-is-dev");
const prepareNext = require("electron-next");
app.on("ready", async () => {
await prepareNext("./renderer");
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false,
preload: join(__dirname, "preload.js"),
},
});
const url = isDev
? "http://localhost:8000"
: format({
pathname: join(__dirname, "../renderer/out/index.html"),
protocol: "file:",
slashes: true,
});
mainWindow.loadURL(url);
});
app.on("window-all-closed", app.quit);
要点逐一展开:
prepareNext("./renderer"):来自electron-next包。它在应用就绪后自动完成两件事——开发环境下在http://localhost:8000启动 Next.js 开发服务器;生产环境下则直接使用renderer目录中已经静态导出的产物。主进程据此把加载路径抽象为统一接口。- 窗口加载地址的二选一:
electron-is-dev判断当前运行环境。开发时加载http://localhost:8000,享受热更新;生产时用url.format把renderer/out/index.html拼装成file://协议 URL 加载,全程不运行 HTTP 服务器。 - 安全基线
nodeIntegration: false:渲染进程不直接注入 Node.js 能力,页面若要触达系统功能只能通过显式的 preload 脚本桥接,这是 Electron 官方推荐的安全实践。 - 生命周期:
window-all-closed时调用app.quit,让应用在窗口全部关闭后退出(macOS 上如需保留 Dock 常驻行为可在此基础上调整)。
静态导出配置:核心中的核心
示例把渲染进程设置为“纯静态导出”,依据在根目录的 next.config.js:
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
output: "export",
};
module.exports = nextConfig;
output: 'export' 是 Next.js 的内置导出开关,其语义在 packages/next/src/server/config-shared.ts 的类型注释中写得很明确:
'export': An exported build output,outdirectory, that only includes static HTML/CSS/JS. Useful for self-hosting without a Node.js server.
也就是说,next build renderer 之后只会得到 renderer/out 下由静态 HTML/CSS/JS 组成的产物(本示例产物入口为 renderer/out/index.html)。这与 Electron 的 file:// 加载方式天然契合,也是示例生产模式“不跑服务器”这一设计能够成立的前提。
需要留意该模式的使用边界:output: 'export' 面向完全静态化的应用。如果你在页面中使用了依赖 Node.js 运行时能力的 API(如动态服务端渲染、Image Optimization 默认服务等),需要迁移到静态兼容方案;从源码结构看,这种取舍正是示例把“路由 + 视图渲染”完全交给 Next.js、而把 Node 侧职责限制在 Electron 主进程内的原因。
从 React 页面安全调用 Electron:preload + contextBridge
页面需要与主进程通信,但示例刻意关闭了 nodeIntegration。为此,main/preload.js 充当安全桥:
const { ipcRenderer, contextBridge } = require("electron");
contextBridge.exposeInMainWorld("electron", {
message: {
send: (payload) => ipcRenderer.send("message", payload),
on: (handler) => ipcRenderer.on("message", handler),
off: (handler) => ipcRenderer.off("message", handler),
},
});
contextBridge.exposeInMainWorld 只向页面注入一个受限对象 window.electron,且仅暴露 message 通道的 send/on/off 三个方法,渲染进程无法触达完整 ipcRenderer。三个方法分别对应:
send(payload):把消息发送给主进程的messageIPC 通道;on(handler):注册回调,接收主进程回传的消息(handler 签名兼容(event, message));off(handler):注销回调,供 React 组件卸载时清理,避免监听器泄漏。
主进程的回显示例
main/index.js 里注册了最简单的 IPC 回显逻辑——收到渲染进程发来的 message 后原样转发回去:
ipcMain.on("message", (event, message) => {
event.sender.send("message", message);
});
页面侧的 React 使用方式
renderer/pages/index.js 演示了完整的「输入 → 发送 → 回显」闭环,以及 useEffect 中订阅/退订的正确姿势:
import { useState, useEffect } from "react";
const Home = () => {
const [input, setInput] = useState("");
const [message, setMessage] = useState(null);
useEffect(() => {
const handleMessage = (event, message) => setMessage(message);
window.electron.message.on(handleMessage);
return () => {
window.electron.message.off(handleMessage);
};
}, []);
const handleSubmit = (event) => {
event.preventDefault();
window.electron.message.send(input);
setMessage(null);
};
return (
<div>
<h1>Hello Electron!</h1>
{message && <p>{message}</p>}
<form onSubmit={handleSubmit}>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
/>
</form>
<style jsx>{`
h1 {
color: red;
font-size: 50px;
}
`}</style>
</div>
);
};
export default Home;
值得注意的细节:
- 页面通过
window.electron这个由 preload 注入的全局对象访问 IPC,而非直接 import Electron 模块,符合关闭nodeIntegration后的安全模型; useEffect中注册监听后必须返回清理函数调用off,否则组件卸载后监听器残留;- 组件还顺带演示了 styled-jsx(Next.js 内置 CSS-in-JS)在 Electron 场景中的正常使用,说明 Web 侧样式方案无需任何改造即可迁移。
针对 Electron 目标的 Babel 编译配置
renderer/babel.config.js 解决了“编译目标对齐”的问题——让转译后的代码与当前安装的 Electron 版本能力匹配:
const { devDependencies } = require("../package.json");
module.exports = {
presets: [
[
"next/babel",
{
"preset-env": {
targets: {
electron: devDependencies.electron.replace(/^\^|~/, ""),
},
},
},
],
],
};
实现思路是:从 package.json 的 devDependencies 中读取 electron 版本(如 ^12.0.2),用正则去掉 ^ / ~ 前缀得到主版本号,然后作为 preset-env 的编译目标传入。这样无论未来如何升级 Electron 依赖,Babel 都会自动针对当前 Electron 内置的 Chromium/Node 版本决定需要转译哪些语法,避免了“为旧版浏览器过度转译”或“语法太新跑不起来”两种失衡。
完整运行与打包工作流
把上面的脚本与配置串起来,得到一套端到端流程:
- 清理(可选):
npm run clean,删除上次的dist、.next、out残留; - 安装依赖:
npm install(注意electron与electron-builder位于 devDependencies); - 本地开发调试:
npm start。Electron 主进程通过electron-next引导 Next.js 开发服务器,窗口加载http://localhost:8000,页面修改即时热更新; - 生产构建 + 目录输出:
npm run pack-app。先next build renderer静态导出,再用electron-builder --dir产出未安装的打包目录,适合快速验证产物内容与启动行为; - 正式分发:
npm run dist。在pack-app基础上生成按当前平台默认目标(如 macOS 的 dmg、Windows 的 nsis、Linux 的 AppImage/deb 等)组织的安装包。
build.files 白名单决定了最终 app.asar 的内容:只有 main/ 主进程代码与 renderer/out 静态页面进入产物;同时主进程入口、窗口参数、IPC 通道、preload 桥接脚本等均在 main/index.js 与 main/preload.js 中集中管理,便于后续扩展更多页面与原生能力。
小结
with-electron 以极小的代码量演示了一种务实、低配置的桌面应用架构范式:Next.js 专注渲染层并静态导出,Electron 主进程专注窗口与系统能力,中间用 preload 脚本做安全的 IPC 桥接。这套「开发时 HTTP + 生产时静态文件」的双模式切换逻辑,以及 electron-builder 的 asar 打包方式,都可以直接平移到你自己的项目中——只需把你的 Next.js 应用放进 renderer/ 子目录、保持 output: "export" 配置,并把主进程入口指向自己的逻辑即可。若需深入理解 output: 'export' 的边界与更多高级配置,可继续阅读仓库中的 next.config.js 类型定义 以及 create-next-app 的示例引导机制。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00