一份使用Electron + Spring Boot构建桌面应用程序的指南

前言

在这份指南中,我将向大家分享如何使用Electron + Spring Boot这样的组合来构建桌面应用程序。除了上述两种技术外,我们还会使用到Vue和Gradle这样的技术。

1. 创建Gradle项目

可以使用Intellij IDEA等IDE进行创建,也可以在命令行中使用如下命令:

gradle init --type java-application

如果使用命令行的方式,需要先下载最新版本的Gradle (https://gradle.org/releases/)并配置好环境变量。

新创建好的项目中包含了build.gradle文件。现在让我们来修改这个文件:

plugins {
    id 'java'
}

group 'cn.gsein'
version '1.0'

repositories {
    mavenCentral()
}

dependencies {
}

2. 在项目中引入Spring Boot

在build.gradle的dependencies中加入Spring Boot相关依赖的坐标:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web:2.4.2'
}

在IDE中刷新Gradle工程,这样就可以看到Spring Boot相关的依赖包被引入项目中。

接下来需要创建Spring Boot的启动类,在src/main/java目录下新建项目的包,如cn.gsein.demo,在新建的包中创建Application启动类:

package cn.gsein.demo;

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;

/**
 * @author G. Seinfeld
 * @since 2021/03/17
 */
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        new SpringApplicationBuilder(Application.class).run(args);
    }
}

从IDE中启动项目,如果没有问题的话,在浏览器中通过http://localhost:8080访问项目,会提示“Whitelabel Error Page”。

3. 在项目中引入Vue和Electron

3.1 安装Node.js和Npm

在开始之前,需要先安装最新版Node.js和Npm

3.1 安装vue-cli3

npm install @vue/cli -g

3.2 创建vue项目

在src/main目录下,执行以下命令,创建vue项目:

vue create electron-vue-demo

3.3 安装electron

进入到项目根目录,执行

vue add electron-builder

3.4 修改项目配置

在项目根目录下创建vue.config.js,粘贴以下代码:

const path = require('path');

function resolve (dir) {
  return path.join(__dirname, dir);
}

module.exports = {
  publicPath: './',
  devServer: {
    // can be overwritten by process.env.HOST
    host: '0.0.0.0',  
    port: 8080
  },
  chainWebpack: config => {
    config.resolve.alias
      .set('@', resolve('src'))
      .set('src', resolve('src'))
      .set('common', resolve('src/common'))
      .set('components', resolve('src/components'));
  }
};

为了取消跨域限制,在background.js中创建窗口时做如下修改:

function createWindow () {
      // Create the browser window.
      win = new BrowserWindow({
        width: 1200,
        height: 620,
        webPreferences: {
+         webSecurity: false,
          nodeIntegration: true
        }
      })

更多细节可以参考https://zhuanlan.zhihu.com/p/75764907

4. Electron和Spring Boot联通性调试

可以在js中增加Electron向Spring Boot发送Http请求的接口,然后IDE中分别启动Electron和Spring Boot进程,测试是否能够正常发送请求。如果发生跨域的问题,请参考第3部分修改配置。

5. Gradle中引入Node、Spring Boot和Application插件

plugins {
    id 'java'
    id 'org.springframework.boot' version "2.4.2"
    id 'com.moowork.node' version "1.3.1"
    id 'application'
}
  • Node插件提供了nodeSetup、npmSetup、npmInstall等任务,可以用于管理Node.js项目,方便将Electron和Spring Boot进行统一管理。
  • Spring Boot插件用于构造Spring Boot应用
  • Application插件用于应用的构造、部署、发布等,还可以用于生成运行java应用的shell或bat启动脚本
  • 当项目中同时存在Spring Boot和Application插件时,Application插件的所有任务将转变为Spring boot版本的任务,如任务startScripts(生成启动脚本)转变为bootStartScripts。

6. 技术细节:怎么实现Electron和Spring Boot进程同时启停?

6.1 启动Electron同时启动Spring Boot

可以使用Node.js的child_process来实现,如下:

let serverProcess
if (isDevelopment) {
  serverProcess = true
} else {
  if (platform === 'win32') {
    serverProcess = require('child_process').spawn('cmd.exe', ['/c', 'redis-client.bat'], {
      cwd: app.getAppPath() + '/bin'
    })
  } else {
    const chmod = require('child_process').spawn('chmod', ['+x', app.getAppPath() + "/bin/redis-client"]);
    chmod.on('close', (code => {
      const chmod2 = require('child_process').spawn('chmod', ['+x', app.getAppPath() + "/runtime/bin/java"]);
      chmod2.on('close', () => {
        serverProcess = require('child_process').spawn(app.getAppPath() + "/bin/redis-client")
      })
    }))
  }
}

也就是说,在Electron启动后,利用Node.js的child_process去执行在构建环节中生成的执行脚本。

6.2 保证Spring Boot进程启动后再打开窗口

我们可以利用Node.js的第三方依赖包minimal-request-promise来检查Spring Boot进程是否已经成功启动。minimal-request-promise可以用来发送http请求,它的体积很小,基本没有其他依赖。我们可以向Spring Boot端一个有效的Url发送请求,如果请求成功,证明进程已启动,可以打开窗口;否则,隔一段时间再次发送请求。

const startUp = function () {
  const requestPromise = require('minimal-request-promise')
  requestPromise.get(appUrl).then(function (response) {
    console.log(response);
    console.log('Server started!');
    createWindow();
    appStarted = true
  }, function (response) {
    console.log(response)
    console.log('Waiting for the server start...');
    setTimeout(function () {
      startUp()
    }, 500)
  })
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', async () => {
  startUp()
})

6.3 保证所有窗口关闭后关闭掉Spring Boot进程

可以使用Node.js的第三方依赖包tree-kill,将Spring Boot端的进程杀掉。

// Quit when all windows are closed.
app.on('window-all-closed', (e) => {
  if (serverProcess && process.platform !== 'darwin') {
    e.preventDefault()
    const kill = require('tree-kill')
    kill(serverProcess.pid, 'SIGTERM', function () {
      console.log('Server process killed')
      serverProcess = null
      app.quit()
    })
  }

})

7. 技术细节:怎么在构建的安装包中内置Jre?

为了在没有安装java(或安装版本不符合要求)的机器上运行应用,需要在应用中内置Java运行环境(Jre)。

7.1 将Jre拷贝到Electron模块的public目录下

可以使用Gradle task中的copy函数进行操作

    var targetDir = project.file("src/main/electron/redis-electron/public")


    var runtimeDir = File(targetDir, "runtime")
    if (runtimeDir.exists()) {
        runtimeDir.delete()
    }
    runtimeDir.mkdir()

    copy {
        from(File(System.getProperty("java.home"), "jre"))
        into(runtimeDir)
    }

7.2 将启动脚本中的JAVACMD修改为Jre中的java命令

Gradle application插件生成的启动脚本默认使用环境变量中配置的java命令,为了使用自定义的命令位置,需要修改启动脚本。

这里我们可以对Gradle application插件进行配置,使用自定义的模板来生成启动脚本。由于我们同时使用了Spring Boot插件,这里需要更改的是Spring Boot插件中的bootStartScripts任务,将任务中unix和windows脚本生成器的模板设定为自定义的模板。

自定义模板的内容可以照抄官方模板,只把涉及到JAVACMD的部分进行修改即可。这里我们以unix版的脚本为例,将其中涉及JAVACMD判断的部分修改为:

    # Determine the Java command to use to start the JVM.
    JAVACMD="\$APP_HOME/runtime/bin/java"

完整的模板可以参考附录中的参考项目

tasks {
    bootStartScripts {
        (unixStartScriptGenerator as TemplateBasedScriptGenerator).template = resources.text.fromFile("customUnixStartScript.txt")
        (windowsStartScriptGenerator as TemplateBasedScriptGenerator).template = resources.text.fromFile("customWindowsStartScript.txt")
    }
}

8. 优缺点

优点:熟悉的技术栈
缺点:应用启动较慢,应用的安装包较大

附录1 参考项目

基于本文技术开发的Redis桌面连接工具

附录2 相关技术简介

Gradle

Electron

Vue

Spring Boot

参考资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容