Electron (02)集成 SpringBoot:服务与桌面程序协同启动方案

发布于:2025-06-22 ⋅ 阅读:(15) ⋅ 点赞:(0)

本篇是关于把springboot生成的jar打到electron里,在生成的桌面程序启动时springboot服务就会自动启动。

虽然之后并不需要这种方案,更好的是部署[一套服务端,多个客户端]...但是既然搭建成功了,也记录一下。

前端文件

1、main.js

const { app, BrowserWindow, ipcMain, Notification, Menu,dialog} = require('electron/main')
const path = require('node:path')
const childProcess = require('child_process');
const fs = require('fs')

let win = null;       // Electron主窗口实例
let backendProcess = null;   // Java子进程实例
const BACKEND_PORT = 8080;   // 后端固定端口(可配置)
const JAR_FILENAME = 'helloworld-0.0.1-SNAPSHOT.jar'; // JAR文件名(需与resources目录下的文件一致)

function writeFile(_, data) {
    fs.writeFileSync('D:/hello.txt', data)
}

function readFile() {
    const res = fs.readFileSync('D:/hello.txt').toString();
    return res
}

/**
 * 获取JAR包路径(兼容开发/生产环境)
 */
function getJarPath() {
    if (app.isPackaged) {
        // 生产环境:打包后,资源目录为process.resourcesPath
        return path.join(process.resourcesPath, 'resources', JAR_FILENAME);
    } else {
        // 开发环境:资源目录为项目根目录的`resources`文件夹
        return path.join(__dirname, 'resources', JAR_FILENAME);
    }
}

/**
 * 4. 启动Java子进程(核心逻辑)
 */
function startBackend() {
    const jarPath = getJarPath();

    // 检查JAR包是否存在(避免启动失败)
    if (!fs.existsSync(jarPath)) {
        dialog.showErrorBox('错误', `JAR包不存在:${jarPath}`);
        app.quit();
        return;
    }

    // 构造Java启动参数(可添加Spring Boot配置,如端口、环境)
    const args = [
        '-jar',
        jarPath,
        `--server.port=${BACKEND_PORT}`,          // 指定后端端口(避免冲突)
        `--spring.profiles.active=prod`           // 指定生产环境配置(可选)
    ];
    // 构造子进程选项(跨平台优化)
    const options = {
        windowsHide: true,  // Windows下隐藏命令行窗口(避免弹出黑框)
        env: { ...process.env }, // 传递环境变量
        cwd: path.dirname(jarPath) // 设置子进程工作目录(避免相对路径问题)
    };

    // 启动子进程(使用spawn,适合长时间运行的进程)
    backendProcess = childProcess.spawn('java', args, options);

    // 5. 监听后端输出(调试用)
    backendProcess.stdout.on('data', (data) => {
        console.log('[Backend]', data.toString().trim());
    });

    // 6. 监听后端错误(如Java未安装、端口冲突)
    backendProcess.stderr.on('data', (data) => {
        const errorMsg = data.toString().trim();
        console.error('[Backend Error]', errorMsg);
        // 处理端口冲突(示例)
        if (errorMsg.includes(`Port ${BACKEND_PORT} is already in use`)) {
            dialog.showErrorBox('错误', `后端端口${BACKEND_PORT}已被占用,请关闭占用程序后重试。`);
            app.quit();
        }
    });

    // 7. 后端退出事件(如异常崩溃)
    backendProcess.on('exit', (code) => {
        console.log('[Backend]', `进程退出,代码:${code}`);
        backendProcess = null;
        // 若后端异常退出,关闭Electron应用
        if (code !== 0 && app.isReady()) {
            dialog.showErrorBox('错误', '后端进程异常退出,请重启应用。');
            app.quit();
        }
    });
}

function createWindow() {
    const win = new BrowserWindow({
        width: 1000,
        height: 800,
        title: '简单网页',
        webPreferences: {
            preload: path.join(__dirname, 'preload.js')
        }
    })
    ipcMain.on('file-save', writeFile)
    ipcMain.handle('file-read', readFile)

    // 加载前端页面(兼容开发/生产环境)
    if (app.isPackaged) {
        console.log('pro')
    } else {
        console.log('dev')
    }

    //自定义菜单项
    let menuTemp = [
        {
            label: '文件',
            submenu: [
                {
                    label: '打开文件',
                    click() {
                        console.log('打开一个具体的文件')
                    }
                },
                { label: '打开文件夹' },
                {
                    label: '关于',
                    role: 'about'
                }
            ]
        },
        { label: '编辑' }
    ]

    //生成自定义菜单
    let menu = Menu.buildFromTemplate(menuTemp)
    Menu.setApplicationMenu(menu)

    win.loadFile('index.html')
    // 创建并显示通知
    const notification = new Notification({
        title: '主进程通知',
        body: '恭喜你,学会了求雨之术,风来~雨来~'
    }).show();

    // 确保在窗口创建后调用 openDevTools
    win.webContents.on('did-finish-load', () => {
      win.webContents.openDevTools();
    });

    // 定时发送时间给渲染进程(每1秒)
    setInterval(() => {
        if (win && !win.isDestroyed()) {
            win.webContents.send('main-time', new Date().toLocaleTimeString());
        }
    }, 1000);
}
app.whenReady().then(
    () => {
        startBackend();       // 启动后端(先启动后端,再创建窗口)
        createWindow();   // 创建主窗口
    })

// 应用退出前确保后端进程终止
app.on('will-quit', () => {
    if (backendProcess) backendProcess.kill();
}); 

2、index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; connect-src 'self' http://localhost:8080;">
<link rel="stylesheet" href="styles.css">
</head>

<body>
    <div id="time">当前时间:加载中...</div>
    <div class="hint">注意:输入内容,可以保存到d:/hello.txt,点击读取,可以读取该文件内容</div>
    <input id="input" type="text">
    <button id="btn2">向D盘输入hello.txt</button>
    <br>
    <br>
    <hr>
    <button id="btn3">读取D盘hello.txt</button>
    <br>
    <br>
    <hr>
    <button id="sendRequest">点击发送请求</button>
    <div id="result"></div>
    <script type="text/javascript" src="./render.js"></script>
</body>

</html>

3、render.js

const timeElement = document.getElementById('time');
const btn2 = document.getElementById('btn2');
const btn3 = document.getElementById('btn3');
const btn4 = document.getElementById('sendRequest');
const resultDiv = document.getElementById('result');
const input = document.getElementById('input');

btn2.onclick = () => {
    myAPI.saveFile(input.value)
}

btn3.onclick = async () => {
    let data = await myAPI.readFile()
    alert(data)
}

// 定义常量
const API_URL = 'http://localhost:8080/getcode';
const METHOD = 'GET';

// 绑定按钮点击事件
btn4.onclick = async () => {
    try {
        // 发送 GET 请求
        const response = await fetch(API_URL, { method: METHOD });

        // 检查响应状态
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        // 解析字符串数据
        const data = await response.text(); // 使用 text() 方法解析字符串
        // 将数据回显到页面上
        resultDiv.innerHTML = `
                    <p class="success">请求成功!<br>返回数据:</p>
                    <pre>${data}</pre>
                `;

    } catch (error) {
        resultDiv.innerHTML = `
                    <p class="error">请求失败,请检查网络或后端服务是否正常运行!</p>
                `;
    }
};

// 监听主进程发送的时间消息
myAPI.onMainTime((time) => {
    timeElement.textContent = `当前时间:${time}`;
});

4、preload.js

const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('myAPI', {
    saveFile: (data) => {
        ipcRenderer.send('file-save', data)
    },
    readFile: () => {
        return ipcRenderer.invoke('file-read')
    },
    // 监听主进程发送的时间消息
    onMainTime: (callback) => {
        ipcRenderer.on('main-time', (event, time) => callback(time))
    }
})

5、package.json

  "scripts": {
    "start": "electron .",
    "build": "electron-builder --win --x64",
    "package": "electron-packager . construction --win --out build --arch=x64 --version1.0.0 --overwrite --icon=static/images/128.ico",
    "make": "electron-forge make"
  },
  "build": {
    "appId": "com.xiaoyumao.demo",
    "extraResources": {
      "from": "resources/helloworld-0.0.1-SNAPSHOT.jar",
      "to": "resources/helloworld-0.0.1-SNAPSHOT.jar"
    },
    "win": {
      "target": [
        {
          "target": "nsis",
          "arch": [
            "x64"
          ]
        }
      ]
    },
    "nsis": {
      "oneClick": false,
      "perMachine": true,
      "allowToChangeInstallationDirectory": true
    }
  },

Electron中集成jar

1、先得有jar包

使用springboot技术,快速生成一个web应用。写一个getcode接口,

    @GetMapping("/getcode")
    public String getcode(){
        UUID randomUUID = UUID.randomUUID();
        String uuidWithoutHyphens = randomUUID.toString().replace("-", "");
        return "随机编码:"+uuidWithoutHyphens;
    }

在浏览器测试的访问一下

没啥问题后,用maven进行打包,生成可以独立运行的jar


2、child_process启动jar

由Electron主进程(Node环境)创建的独立进程,来启动jar

child_process.spawn()

用于创建一个子进程并实时监听其输入和输出。

java -jar C:\Users\lenovo\electron-basics\resources\helloworld-0.0.1-SNAPSHOT.jar --server.port=8080

3、resource目录

还需要在package.json配置extraResources ,用于在构建 Electron 应用程序时将额外的资源文件打包到最终的应用程序安装包中。它的主要作用是确保应用程序所需的资源文件能够正确地随应用一起发布,而不会丢失。

    "extraResources": {
      "from": "resources/helloworld-0.0.1-SNAPSHOT.jar",
      "to": "resources/helloworld-0.0.1-SNAPSHOT.jar"
    },

在 Electron 中,process.resourcesPath 指向的是应用程序的资源目录。

    if (app.isPackaged) {
        // 生产环境:打包后,资源目录为process.resourcesPath
        return path.join(process.resourcesPath, 'resources', JAR_FILENAME);
    } 

在这里资源文件都放在了electron本身生成的resources目录中


4、假如没有JAVA_HOME环境

有些情况就是,电脑它没有javahome环境,或者有但是配置的不是我们想要的jdk1.8。。

所以我决定打包的时候把jre环境也打进去,jar启动原理就是下面这样的

C:\Users\lenovo\electron-basics\resources\jre\bin\java -jar C:\Users\lenovo\electron-basics\resources\helloworld-0.0.1-SNAPSHOT.jar --server.port=8080

在resource目录下把jre环境放进去。

package.json就得改变了

    "extraResources": [
      {
        "from": "resources/jre",
        "to": "resources/jre"
      },
      {
        "from": "resources/helloworld-0.0.1-SNAPSHOT.jar",
        "to": "resources/helloworld-0.0.1-SNAPSHOT.jar"
      }
    ],

在main.js中,关于启动jar包的命令、对java环境的检查等都要用xxx\jre\bin\java去检查

在安装软件后,目录是这样的

现在就算没有JAVA_HOME,也照样可以运行


JSP应用

如果项目之前是用jsp写的,那么能不能啥都不改的情况下,直接访问l


网站公告

今日签到

点亮在社区的每一天
去签到