2023年5月11日
By: Chase

electron打包前后端

目录

init项目

node版本v18.16.0

前端初始化

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

使用任意你打算使用的前端框架, 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>
		</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'))
})
Tags: electron 打包