首页
/ 基于 Next.js 构建 Electron 桌面应用:with-electron 官方示例完整指南

基于 Next.js 构建 Electron 桌面应用:with-electron 官方示例完整指南

2026-09-07 17:01:27作者:薛曦旖Francesca

在本仓库的 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 的 .nextout 缓存目录
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

  • dependencieselectron-is-dev(判断当前是否开发环境)与 electron-next(负责引导 Next.js 的开发服务器 / 静态渲染);
  • devDependencieselectronelectron-buildernextreactreact-dom

electron-builder 的配置同样内联在 package.jsonbuild 字段中:

"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);

要点逐一展开:

  1. prepareNext("./renderer"):来自 electron-next 包。它在应用就绪后自动完成两件事——开发环境下在 http://localhost:8000 启动 Next.js 开发服务器;生产环境下则直接使用 renderer 目录中已经静态导出的产物。主进程据此把加载路径抽象为统一接口。
  2. 窗口加载地址的二选一electron-is-dev 判断当前运行环境。开发时加载 http://localhost:8000,享受热更新;生产时用 url.formatrenderer/out/index.html 拼装成 file:// 协议 URL 加载,全程不运行 HTTP 服务器
  3. 安全基线 nodeIntegration: false:渲染进程不直接注入 Node.js 能力,页面若要触达系统功能只能通过显式的 preload 脚本桥接,这是 Electron 官方推荐的安全实践。
  4. 生命周期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, out directory, 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):把消息发送给主进程的 message IPC 通道;
  • 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.jsondevDependencies 中读取 electron 版本(如 ^12.0.2),用正则去掉 ^ / ~ 前缀得到主版本号,然后作为 preset-env 的编译目标传入。这样无论未来如何升级 Electron 依赖,Babel 都会自动针对当前 Electron 内置的 Chromium/Node 版本决定需要转译哪些语法,避免了“为旧版浏览器过度转译”或“语法太新跑不起来”两种失衡。

完整运行与打包工作流

把上面的脚本与配置串起来,得到一套端到端流程:

  1. 清理(可选)npm run clean,删除上次的 dist.nextout 残留;
  2. 安装依赖npm install(注意 electronelectron-builder 位于 devDependencies);
  3. 本地开发调试npm start。Electron 主进程通过 electron-next 引导 Next.js 开发服务器,窗口加载 http://localhost:8000,页面修改即时热更新;
  4. 生产构建 + 目录输出npm run pack-app。先 next build renderer 静态导出,再用 electron-builder --dir 产出未安装的打包目录,适合快速验证产物内容与启动行为;
  5. 正式分发npm run dist。在 pack-app 基础上生成按当前平台默认目标(如 macOS 的 dmg、Windows 的 nsis、Linux 的 AppImage/deb 等)组织的安装包。

build.files 白名单决定了最终 app.asar 的内容:只有 main/ 主进程代码与 renderer/out 静态页面进入产物;同时主进程入口、窗口参数、IPC 通道、preload 桥接脚本等均在 main/index.jsmain/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 的示例引导机制。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.74 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
595
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.63 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
518
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
389