目录

init项目

前端初始化

如果你已熟悉前端的开发与生产环境的启动部署, 可以快速跳过本步骤.

使用任意你打算使用的前端框架, create-react-app(CRA) , vue-cli 等等.

前端变化太快, 之前react官网一直把 CRA 当亲儿子, 首推这个, 我搭建的两个electron项目前端框架也都是用它. 没想到如今react官网已然看不到推荐使用 CRA 搭建项目了. 现在推荐用nextJS.

于是我这里初始化了一个 nextJS 玩玩, 版本为 13.4.1 , 配置选择如下. 图 1

根据官方文档的介绍, 在 src/app 目录下添加一个 /dashboard 的前端路由页面, 又稍微改动了一下开发端口(3001)与打包参数(静态文件打包). 图 4

现在我们启动开发环境是 npm run dev 之后的 3001 端口, 生产环境是打包出来的 build 文件夹下的静态文件.

添加electron和相关依赖

yarn add -D concurrently wait-on electron electron-builder
yarn add electron-is-dev express
名称 说明
electron 呃, 就是electron
electron-builder 打包electron应用
express 生产环境中, 启前端用的
concurrently 开发环境中, 自动启前端再启electron用的
wait-on 开发环境中, 自动启前端再启electron用的
electron-is-dev 判断electron目前处于开发还是生产环境

开发环境启动electron

官网介绍的非常清晰简洁了: electron的quick-start

简单来说2个改动:

  1. package.json 添加入口文件和启动脚本.
{
  "main": "public/electron.js",  
  "scripts": {
    "dev": "next dev -p 3001",
    "build": "next build",
    "start-electron": "electron .",
    "start-all": "concurrently \"npm run dev\" \"wait-on http://localhost:3001 && npm run start-electron\""
  1. /public下添加 electron.js
const { app, BrowserWindow } = require('electron')
const isDev = require('electron-is-dev')
let mainWindow
const createWindow = () => {
    // 主窗口参数设置
    mainWindow = new BrowserWindow({
        fullscreenable: true,
        fullscreen: true,
        webPreferences: {
            nodeIntegration: true
        titleBarStyle: 'default',
        autoHideMenuBar: true,
        title: 'demo',
        maximizable: true,
        resizable: true,
        show: false
    try {
        if (!isDev) {
            const productPort = 8013
            mainWindow.loadURL(`http://localhost:${productPort}`)
        } else {
            mainWindow.loadURL('http://localhost:3001')
    } catch (error) {
        console.log('error happened when start frontend', error)
    mainWindow.on('ready-to-show', () => {
        mainWindow.show()
app.whenReady().then(createWindow)
app.on('window-all-closed', () => {
    console.log('close app')
    if (process.platform !== 'darwin') {
        console.log('app quit')
        app.quit()
    process?.exit()

改动完毕, 尝试启动. 上面第1个改动中, 看到我添加了一个

"start-all": "concurrently \"npm run dev\" \"wait-on http://localhost:3001 && npm run start-electron\""

就是把启动前端和启动electorn的两个命令合并了, 完全可 npm run dev 跑完了, 再跑 npm run start-electron .

如果对 electron 的启动还不是很熟悉, 又或者有问题与疑问, 建议分开跑, 每次只重新跑一下 npm run start-electron 即可, 而且在electron.js里可以打log看看执行.

上面的启动electron的代码

mainWindow.loadURL('http://localhost:3001')

如果看明白了, 实际就能理解, electorn 就是壳, localhost:3001 是肉. 剥离 electron 的包裹, 剩下的就是我们传统的web前端开发.

生产环境启动

如何启动前端

传统的web生产环境部署, 用 nginx , serve (CRA推荐)或者 express , pm2 等等方式很多.

因为electron自带node环境, 我这里直接用 express 了.

public/electron.js 新增部分代码

const path = require('path')
const express = require('express')
const appEx = express()
let mainWindow
const createWindow = () => {
    try {
        if (!isDev) {
            const productPort = 8013
            // 用express启生产环境前端
            const reactBuildPath = path.join(__dirname, '../build')
            appEx.use(express.static(reactBuildPath))
            appEx.get('/*', (_, res) => {
                res.sendFile(path.join(__dirname, '../build/index.html'))
            appEx.listen(productPort)
            console.log(`server is running at ${productPort} port`)
            mainWindow.loadURL(`http://localhost:${productPort}`)
        } else {
            mainWindow.loadURL('http://localhost:3001')
    } catch (error) {
        console.log('error happened when start frontend', error)

就是用 express 启动生产环境, 然后 electron 再包裹载入生产环境的端口.

具体的启动生产环境的代码, 可能因不同的前端框架, 打包出来的结构有出入, 需要做一些调整. 至于如何调试启动开发环境, 简单的办法就是在 build 文件下建一个 test.js 文件, 把启动要用的js代码复制进去, 直接 node test.js , 模拟测试看看项目成功启动与否.

打包electron

electron-builder 启动, 详细的参数了解请看官方文档.

我这里说我修改的一些很基础的地方, package.json内添加一些脚本与设置

  "main": "build/electron.js", // 这里生产环境打包进去后, 目录变为build/elctron.js
  "scripts": {
    "start-all": "concurrently \"npm run dev\" \"wait-on http://localhost:3001 && npm run start-electron\"",
    "build-electron": "electron-builder --dir", // 这里负责打免安装版的electron
    "build-all": "npm run build && npm run build-electron" 
  "build": {
    "appId": "demo",
    "npmRebuild": true,
    "asar": false,
    "mac": {
      "category": "tools"
    "files": [
      "build/**/*",
      "package.json"

npm run build-all 即可.

生产环境启动后端

设计思路: 我们的前端是用 node 环境, 跑 express 启的, 后端同理, 也可以在 electron.js 内启动. 同时为了保证前后端一起关闭, 利用node的 childProcess , 关联前端主进程即可.

开发环境测试后端接入

我这里在项目下建立一个backend文件夹, 简单写个接口模拟一下后端, 调用 locaclhost:3003 就会返回个当前时间 图 7

cd ./backend
node index.js

后端起来后, 在我们的前端dashboard页面加入一个接口请求

"use client"
import { useEffect, useState } from 'react'
import styles from './style.module.css'
export default function Dashboard() {
	const [state, setState] = useState('')
	useEffect(() => {
		getTimeFromRequest()
	}, [])
	const getTimeFromRequest = async () => {
		const response = await fetch('http://localhost:3003')
		const time = await response.text()
		setState(time)
	return (
		<div className={styles.red}>
			dashboard page
			<div>{`current Time: ${state}`}</div>

到'localhost:3001/dashboard'就能看到请求是成功的 图 8

打包进生产环境

// 引入child_process
const exec = require('child_process')
let childProcess
// 启动前端
mainWindow.loadURL(`http://localhost:${productPort}`)
// 用node启后端环境
const backendPath = path.join(__dirname, './backend')
childProcess = exec.spawn(
    'node',
    ['index.js'],
    { cwd: backendPath }
childProcess.stdout.on('data', (data) => {
    console.log(`stdout: ${data}`);
childProcess.stderr.on('data', (data) => {
    console.error(`stderr: ${data}`);
childProcess.on('close', (code) => {
    console.log(`child process exited with code ${code}`);
app.on('window-all-closed', () => {
    console.log('close app')
    // 结束时增加kill子进程指令
    if (childProcess) {
        childProcess.kill()
        console.log('close backend childProcess')
    if (process.platform !== 'darwin') {
        console.log('app quit')
        app.quit()
    process?.exit()

package, 新增打包命令脚本, 在前端打包完毕后, 将后端代码复制到build文件夹下

  "build": "next build",
  "cp-backend": "cp -r ./backend ./build",
  "build-electron": "electron-builder --dir",
  "build-all": "npm run build && npm run cp-backend && npm run build-electron" 

跑一下 npm run build-all , 就好啦. 启动康康: 图 9

java或其他启动的后端

我上面的例子是node启后端, electron有node环境, 所以不需要再安装其他. 像我们公司的项目, 大部分后端用clojure开发, 要启jar包. 那就需要把jdk也打进去(除非你的安装机器已有java环境).

贴部分代码供参考一下:

// 子进程启动java
const javaStartCmd = ['-jar', 'device-sensor.jar']
const javaBuildPath = path.join(__dirname, '../build/backend')
// java路径为
const java17Path = path.join(__dirname, '../build/jdk-17.0.1/bin')
childProcess = exec.spawn(
    `${java17Path}/java`,
    javaStartCmd,
        cwd: javaBuildPath,
        env: {
            detached: false,
            LANG: 'zh_CN.UTF-8'

上面代码表示我的build文件夹内是有 jdk-17.0.1 的java环境的, 所以在复制 backend 也别忘了把 jdk 拷贝进去.

这里拓展链接一下之前 java环境变量的引发的bug

electron打包调试

其中打包electron可能会遇到问题, 我在这里给出几个常见问题与调试建议:

查看打包后的原始文件

asar 在调试中改为false, 这样可以在 dist/mac/electron-next-demo.app/Contents/Resources/app 内, 直接看到你打包进去的文件, 也许可以解决包括以下并不限于的问题:

  1. 某些文件打包打丢了
  2. 打包完的路径和预想的不一样, 有时也会有绝对路径, 相对路径的问题 比如我的 打包electron 内, 就把package.json的 public/electorn.js -> build/electron.js , 因为打包进 Resources/app 后没有public文件夹了

命令行启动查看log

用命令行启动你的免安装包软件, 这样可以看到 electron.js 内的log. 只要你的log够详细, 很快就可以定位到是哪几行代码导致的启动electron出错, 如下图 图 5

利用个熟悉的浏览器调试窗口

electron窗口起来后, 可以调出熟悉的调试窗口, 看看前端报错. 很多人遇到过白屏的错误, 如果是静态资源加载的路径不对, 这里也是能看出端倪的. 图 6

避免前端路由带来的生产环境启动问题

生产环境的前端跳转有问题. 这里就要搞清楚你的项目有没有用到前端路由, 以及启生产环境是怎么启动的. 像我这里的 /* / 的差别, 就是解决前端路由的启动.

appEx.get('/*', (_, res) => {
    res.sendFile(path.join(__dirname, '../build/index.html'))