本篇是關于把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.resourcesPathreturn 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();// 確保在窗口創建后調用 openDevToolswin.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.resourcesPathreturn 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