SpringBoot使用Chromium开发桌面客户端

架构设计

  1. 将B/S前后端的概念利用在C/S端
  2. 使用纯前端展示UI界面
  3. 使用SpringBoot REST API 作为后端接口
  4. 前端使用 ajax/fetch 获取后端数据或者触发后端行为
  5. 后端通过发布订阅方式主动推送数据给前端
  6. 后端利用JCEF调用Chromium作为前端运行环境

搭建开发环境

  1. IDE使用的是IDEA 2021.2.2
  2. SpringBoot使用的是 2.3.7.RELEASE 版本
  3. Chromium使用的是 110.0.25.1 版本

后端依赖

<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>me.friwi</groupId>
        <artifactId>jcefmaven</artifactId>
        <version>110.0.25.1</version>
    </dependency>
</dependencies>

前端依赖

<script src="https://cdn.bootcdn.net/ajax/libs/pubsub-js/1.9.4/pubsub.min.js"></script>

下载chromium

  1. 进入 https://developer.aliyun.com/mvn/search
  2. 仓库选择 central,关键字选择 jcef-natives,点击搜索
    仓库文件
  3. ctrl+f 搜索 110.0.25 找到 artifact-id 为 jcef-natives-windows-amd64,packaging为jar的项
  4. 点击右边的下载题标,获取jar
  5. 解压jar,里面有个tar包,再解压tar包
    jar包内容
  6. 将解压的文件放在一个文件夹里,文件夹拖到springboot工程下

代码

  1. SpringBoot启动类
package com.example.jcefdemo;

import lombok.extern.slf4j.Slf4j;
import me.friwi.jcefmaven.CefAppBuilder;
import me.friwi.jcefmaven.MavenCefAppHandlerAdapter;
import org.cef.CefApp;
import org.cef.CefClient;
import org.cef.browser.CefBrowser;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;
import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;

@SpringBootApplication
@Slf4j
public class App {

    private static CefBrowser browser;

    private static int port = 8080;

    private static String chromiumDir = "chromium";
    
    public static void main(String[] args) {
        ConfigurableApplicationContext context = new SpringApplicationBuilder(App.class)
                .headless(false).run(args);
        createUI(context,args);
    }

    private static void createUI(ConfigurableApplicationContext context, String[] args) {
        try {
            CefAppBuilder builder = new CefAppBuilder();
            builder.getCefSettings().windowless_rendering_enabled = false;
            builder.setAppHandler(new MavenCefAppHandlerAdapter() {
                @Override
                public void stateHasChanged(org.cef.CefApp.CefAppState state) {
                    // 关闭应用时退出jvm运行
                    if (state == CefApp.CefAppState.TERMINATED) exit(context);
                }
            });

            // 设置cef运行参数,这里为空
            builder.addJcefArgs(args);

            // 设置 cef chrome实例的目录,关键,若不设置他会默认从网络中下载,国外网络可能下载不稳定导致失败
            builder.setInstallDir(new File(System.getProperty("user.dir") + File.separator + chromiumDir));

            // 由于是手动设置cef的chrome,我们要跳过ins检查,防止版本不一致导致从镜像站下载
            builder.setSkipInstallation(true);

            // 全局的 CefApp 每个程序只能有一个,线程安全
            CefApp build = builder.build();
            // 显示一些版本信息
            CefApp.CefVersion cefVersion = build.getVersion();
            log.info(cefVersion.toString());

            // 创建一个浏览器客户端实例
            CefClient client = build.createClient();

            // 创建一个浏览器实例,设置访问 url
            browser = client.createBrowser("http://localhost:"+port, false, true);

            // 获取UI组件
            java.awt.Component uiComponent = browser.getUIComponent();

            // 创建 JFrame UI 用于放入chrome
            JFrame main = new JFrame("jcef demo");
            main.getContentPane().add(uiComponent, BorderLayout.CENTER);
            main.setSize(1280, 720);//大小
            main.setLocation(0, 0);//位置

            main.setVisible(true);//显示
            main.addWindowListener(new WindowAdapter() {
                @Override
                public void windowClosing(WindowEvent e) {
                    // 关闭应用时要释放资源
                    CefApp.getInstance().dispose();
                    main.dispose();
                    exit(context);
                }
            });
        }catch (Exception e) {
            e.printStackTrace();
            exit(context);
        }
    }

    private static void exit(ConfigurableApplicationContext context) {
        if (context.isActive()) {
            SpringApplication.exit(context);
            System.exit(0);
        }
    }

    public static void publish(String name, String data) {
        String js = "PubSub.publish('"+name+"','"+data+"')";
        browser.executeJavaScript(js,"",0);
    }
}
  1. 测试用的Controller
package com.example.jcefdemo.controller;

import com.example.jcefdemo.App;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.swing.*;
import java.io.File;

@RestController
public class TestController {

    @Autowired
    private ObjectMapper objectMapper;

    @GetMapping("/hello")
    public ObjectNode hello() {
        ObjectNode objectNode = objectMapper.createObjectNode();
        objectNode.put("name","hello");
        return objectNode;
    }

    @GetMapping("/selectFile")
    public void selectFile() {
        SwingUtilities.invokeLater(() -> {
            JFileChooser fileChooser = new JFileChooser();
            int result = fileChooser.showOpenDialog(null);
            // 处理用户的选择
            if (result == JFileChooser.APPROVE_OPTION) {
                File selectedFile = fileChooser.getSelectedFile();
                String path = selectedFile.getAbsolutePath().replace("\\","/");
                App.publish("fileChooser",path);
            }
        });
    }
}
  1. 在resources/static目录下新建index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/pubsub-js/1.9.4/pubsub.min.js"></script>
</head>
<body>
<p>hello world</p>
<p id="p1"></p>
<p id="p2"></p>
<button onclick="fetch('/selectFile')">打开</button>
<script>
    fetch('/hello').then(r => r.json()).then(d => {
        document.getElementById('p1').innerText = d.name;
    })

    PubSub.subscribe('fileChooser', function (message, data) {
        document.getElementById('p2').innerText = data;
    });
</script>
</body>
</html>
posted @ 2023-10-09 16:55  小小爬虫  阅读(542)  评论(0编辑  收藏  举报