技术分享 | 在 IDE 插件开发中接入 JCEF 框架

news2024/12/27 11:04:37

项目背景

当前的开发环境存在多种不同语言的 IDE,如 JetBrains 全家桶、Eclipse、Android Studio 和 VS Code 等等。由于每个 IDE 各有其特定的语言和平台要求,因此开发 IDE 插件时,需要投入大量资源才能尽可能覆盖大部分工具。同时,代码难复用、用户体验难统一等问题又会进一步加重资源负担。

在调研过程中,我们发现如今的大多数开发工具都支持集成 CEF,而 CEF 提供的跨平台解决方案正可以有效解决上述问题。

关于 CEF 和 JCEF

CEF(Chromium Embedded Framework)是一个开源项目,它基于 Google Chromium 浏览器引擎,允许开发人员将完整的浏览器功能嵌入到自己的应用程序中。

通过 CEF,开发者可以利用现代 Web 技术来创建强大的桌面应用程序,并实现与 Web 内容的无缝集成。如此一来,开发者便可以利用 CEF 的功能和灵活性,为各种开发工具提供统一的、高质量的插件体验。

JCEF(Java Chromium Embedded Framework)是基于 CEF 的一个特定版本,专门为 Java 应用程序而生。本文内容也主要围绕 JCEF 展开。

JCEF 和其他产品的对比

  • JCEF vs JxBrowser

JxBrowser 和 JCEF 都允许将 Chromium 浏览器功能嵌入到 Java 应用程序中。其中,JxBrowser 是商业产品,而 JCEF 是开源框架,且商业授权非常友好。

此外,JxBrowser 在独立的本地进程中启动 Chromium,而 JCEF 则是在 Java 进程内启动。JCEF 会快速初始化 Chromium,同时消耗 Java 进程的内存和 CPU;创建多个 Chromium 实例也会占用更多资源。

  • JCEF vs JavaFX

JavaFX 使用的内置浏览器组件是 WebView,其在不同平台上的实现有所不同。例如,在 macOS 上使用 WebKit,在 Windows 上默认为 Internet Explorer,而新版本的 JavaFX 则默认使用 JCEF。

这种不一致性会增加插件适配的难度,降低整体开发效率。

Java 进程与 JCEF 交互

如何在 IDE 插件中接入 JCEF?

下面以 LigaAI Jetbrains 插件为例,介绍集成 JCEF 的过程。

  1. 在 Java 代码里创建相应的 JcefBrowser
static JBCefBrowser createBrowser(Project project) {
    JBCefClient client = JBCefApp.getInstance().createClient();
    //CefMessageRouter 用于处理来自 Chromium 浏览器的消息和事件,
    //前端代码可以通过innerCefQuery和innerCefQueryCancel发起消息给插件进行处理
    CefMessageRouter.CefMessageRouterConfig routerConfig =
            new CefMessageRouter.CefMessageRouterConfig("innerCefQuery", "innerCefQueryCancel");
    CefMessageRouter messageRouter = CefMessageRouter.create(routerConfig, new MessageRouterHandler());
    client.getCefClient().addMessageRouter(messageRouter);
    //用于处理以http://inner/开头的请求。 用于拦截特定请求,转发请求到本地以获取本地资源
    CefApp.getInstance()
            .registerSchemeHandlerFactory("http", "inner", new DataSchemeHandlerFactory());
    return new JBCefBrowser(client, "");
}
  1. 加载对应的 URL,渲染页面。
public static void loadURL(JBCefBrowser browser, String url) {
    //如果不需要设置和浏览器显示相关的,可忽略
    browser.getJBCefClient()
            .addDisplayHandler(settingsDisplayHandler, browser.getCefBrowser());
    browser.loadURL(url);                 
}
  1. Java 进程拦截前端发起的获取静态资源的请求。如果直接访问外部资源,则不需要做拦截,这一步可忽略。
import com.intellij.liga.web.WebviewClosedConnection;
import com.intellij.liga.web.WebviewOpenedConnection;
import com.intellij.liga.web.WebviewResourceState;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import org.apache.commons.lang.StringUtils;
import org.cef.callback.CefCallback;
import org.cef.handler.CefResourceHandler;
import org.cef.misc.IntRef;
import org.cef.misc.StringRef;
import org.cef.network.CefRequest;
import org.cef.network.CefResponse;

import java.io.File;
import java.net.URL;


//继承 CefResourceHandler 接口,自定义处理 Chromium 浏览器加载的资源(如网页、图像、样式表等)。
//通过实现该接口,可以覆盖默认的资源加载行为,并提供自定义的资源加载逻辑。
public class DataResourceHandler implements CefResourceHandler {

    private WebviewResourceState state;

    /**
    * 用于处理资源请求,你可以通过该方法获取请求的 URL、请求头部信息,并返回相应的响应结果。
    */
    public boolean processRequest(CefRequest cefRequest, CefCallback cefCallback) {
        String url = cefRequest.getURL();
        //判断请求是否是用于获取内部静态资源的,如果是则拦截请求,并从项目里对应配置获取对应文件返回
        //如果是请求外部资源,则跳过
        if (StringUtils.isNotBlank(url) && url.startsWith("http://inner")) {
            String pathToResource = url.replace("http://inner", "/front/inner");
            pathToResource = pathToResource.split("\\?")[0];
            URL resourceUrl = getClass().getResource(pathToResource);
            VirtualFile f = VfsUtil.findFileByURL(resourceUrl);
            resourceUrl = VfsUtil.convertToURL(f.getUrl());
            try {
                this.state = (WebviewResourceState) new WebviewOpenedConnection(resourceUrl.openConnection());
            } catch (Exception exception) {
                //log output
            }
            cefCallback.Continue();
            return true;
        }
        return false;
    }

    /**
    * 用于设置资源响应的头部信息,例如 Content-Type、Cache-Control 等。
    */
    public void getResponseHeaders(CefResponse cefResponse, IntRef responseLength, StringRef redirectUrl) {
        this.state.getResponseHeaders(cefResponse, responseLength, redirectUrl);
    }

    /**
    * 用于读取资源的内容,可以从这个方法中读取资源的数据并将其传递给浏览器
    */
    public boolean readResponse(byte[] dataOut, int designedBytesToRead, IntRef bytesRead, CefCallback callback) {
        return this.state.readResponse(dataOut, designedBytesToRead, bytesRead, callback);
    }

    /**
    * 请求取消
    */
    public void cancel() {
        this.state.close();
        this.state = (WebviewResourceState) new WebviewClosedConnection();
    }

}

//定义处理 Chromium Embedded Framework (CEF) 中的 Scheme(协议)请求
public class DataSchemeHandlerFactory implements CefSchemeHandlerFactory {
    public CefResourceHandler create(CefBrowser cefBrowser, CefFrame cefFrame, String s, CefRequest cefRequest) {
        return new DataResourceHandler();
    }
}



import org.cef.callback.CefCallback;
import org.cef.handler.CefLoadHandler;
import org.cef.misc.IntRef;
import org.cef.misc.StringRef;
import org.cef.network.CefResponse;

import java.io.InputStream;
import java.net.URLConnection;

public class WebviewOpenedConnection implements WebviewResourceState {
    private URLConnection connection;

    private InputStream inputStream;

    public WebviewOpenedConnection(URLConnection connection) {
        this.connection = connection;
        try {
            this.inputStream = connection.getInputStream();
        } catch (Exception exception) {
            System.out.println(exception);
        }
    }

    public void getResponseHeaders(CefResponse cefResponse, IntRef responseLength, StringRef redirectUrl) {
        try {
            String url = this.connection.getURL().toString();
            cefResponse.setMimeType(this.connection.getContentType());
            try {
                responseLength.set(this.inputStream.available());
                cefResponse.setStatus(200);
            } catch (Exception e) {
                cefResponse.setError(CefLoadHandler.ErrorCode.ERR_FILE_NOT_FOUND);
                cefResponse.setStatusText(e.getLocalizedMessage());
                cefResponse.setStatus(404);
            }
        } catch (Exception e) {
            cefResponse.setError(CefLoadHandler.ErrorCode.ERR_FILE_NOT_FOUND);
            cefResponse.setStatusText(e.getLocalizedMessage());
            cefResponse.setStatus(404);
        }
    }

    public boolean readResponse(byte[] dataOut, int designedBytesToRead, IntRef bytesRead, CefCallback callback) {
        try {
            int availableSize = this.inputStream.available();
            if (availableSize > 0) {
                int maxBytesToRead = Math.min(availableSize, designedBytesToRead);
                int realNumberOfReadBytes = this.inputStream.read(dataOut, 0, maxBytesToRead);
                bytesRead.set(realNumberOfReadBytes);
                return true;
            }
        } catch (Exception exception) {
            //log output
        } finally {
            this.close();
        }
        return false;
    }

    public void close() {
        try {
            if (this.inputStream != null)
                this.inputStream.close();
        } catch (Exception exception) {
            //log output
        }
    }
}
  1. 前端发送请求调用插件,Java 进程接收并处理。
//前端示例代码
<button onclick="callBrowser()">调用浏览器代码</button>

<script>
function callBrowser() {
  var parameter = "example parameter";
  window.location.href = "innerCefQuery://" + parameter;
}
</script>


//插件示例代码
import org.cef.browser.CefBrowser;
import org.cef.browser.CefFrame;
import org.cef.callback.CefQueryCallback;
import org.cef.handler.CefMessageRouterHandlerAdapter;

public class MessageRouterHandler extends CefMessageRouterHandlerAdapter {
    @Override
    public boolean onQuery(CefBrowser browser, CefFrame frame, long query_id, String request,
                           boolean persistent, CefQueryCallback callback) {
        try {
            System.out.println(request);
            callback.success("");
            return true;
        } catch (Exception e) {
            //log output
        }
        return false;
    }
}
  1. 插件前端代码。
// java进程调用前端代码
String script = "window.postMessage('" + JSONObject.toJSONString(scriptObj) + "');";
browser.executeJavaScript(script, "", 0);

// 前端代码
function postMessage(data) {
  // 处理从后端传递过来的数据
  console.log('Received message from backend:', data);
  // 在这里进行你希望执行的其他操作
}

实现效果

通过使用 LigaAI IDE 插件,开发者们无需跳转或登录外部系统,在 IDE 内就能查看任务详情、完成工作、更新和同步任务状态、记录并提报完成信息;在享受沉浸式工作的同时,零负担地实现个人目标管理。

此外,JCEF 为插件开发者提供了一个强大的工具,可以利用 Chromium 浏览器的各种功能和扩展性,以更丰富、更高级的方式提供信息和功能,使编码过程变得容易。

因此,利用 LigaAI IDE 插件提供的可视化图表,研发团队还可以了解整体编码情况、不同任务类型的耗时分布等,更有针对性地制定优化方案,或调整规划排期。

常见问题及避坑指南

1:集成 JCEF,如何使 Web 样式与 IDE 插件整体样式保持统一?

通过下述方法获取 IDE 的主题模式;

public static String getGlobalStyle() {
    if (EditorColorsManager.getInstance().isDarkEditor())
        return "dark";
    return "light";
}

获取 IDE 内的样式。

//主要可以查看com.intellij.util.ui.UIUtil和com.intellij.ui.JBColor这两个类
//获取字体大小
Font font = UIUtil.getLabelFont();
//获取背景颜色
Color bg = JBColor.background();
//获取字体颜色
Color labelFontColor = UIUtil.getLabelFontColor(UIUtil.FontColor.NORMAL);
//获取按钮的背景颜色
JBColor buttonBg = JBColor.namedColor("Button.default.startBackground",JBUI.CurrentTheme.Focus.defaultButtonColor());
//获取边框的颜色
Color border = JBColor.border();

2:Java 和浏览器之间的交互路由名称不能设置为 cefQuerycefQueryCancel

这两个为 JCEF 的内置路由,同名会干扰甚至覆盖 JCEF 的内部处理逻辑,有一定概率会导致系统白屏等意外行为和异常情况。

CefMessageRouter.CefMessageRouterConfig routerConfig =
            new CefMessageRouter.CefMessageRouterConfig("innerCefQuery", "innerCefQueryCancel");

3:于 JetBrains 插件而言,如果浏览器加载的静态页面数据是打包在插件包内的本地数据,加载过程中获取目标 URL 需要先把目标文件转化为 JetBrains 的虚拟文件,再获取虚拟文件的 URL 作为结果,不然会加载不到目标文件。

public boolean processRequest(CefRequest cefRequest, CefCallback cefCallback) {
    String url = cefRequest.getURL();
    if (StringUtils.isNotBlank(url) && url.startsWith("http://inner")) {
        String pathToResource = url.replace("http://inner", "/front/inner");
        pathToResource = pathToResource.split("\\?")[0];
        // 这里先获取目标文件,转成虚拟文件,再获取对应URL
        URL resourceUrl = getClass().getResource(pathToResource);
        VirtualFile f = VfsUtil.findFileByURL(resourceUrl);
        resourceUrl = VfsUtil.convertToURL(f.getUrl());
        //
        try {
            this.state = (WebviewResourceState) new WebviewOpenedConnection(resourceUrl.openConnection());
        } catch (Exception exception) {
        }
        cefCallback.Continue();
        return true;
    }
    return false;
}

4:插件初始化时,如果浏览器请求 java 的接口较多,或接口速度较慢时,可能会出现白屏。这是因为 onQuery 里复杂的逻辑需要异步处理,不然多个请求会阻塞导致浏览器白屏。

public class MessageRouterHandler extends CefMessageRouterHandlerAdapter {
    @Override
    public boolean onQuery(CefBrowser browser, CefFrame frame, long query_id, String request,
                           boolean persistent, CefQueryCallback callback) {
        try {
            ApplicationManager.getApplication().invokeLater(() -> {
                //进行复杂的逻辑
            });
            callback.success("");
            return true;
        } catch (Exception e) {
            //log output
        }
        return false;
    }
}

参考资料

[1] CEF 相关文档:https://github.com/chromiumembedded/cef

[2] JCEF 源码位置: https://github.com/chromiumembedded/java-cef

[3] Jetbrains 插件开发文档:https://plugins.jetbrains.com/docs/intellij/welcome.html

[4] JxBrowser 和 JCEF 的对比:https://dzone.com/articles/jxbrowser-and-jcef


了解更多技术干货、研发管理实践等分享,请关注 LigaAI。

欢迎试用 LigaAI-智能研发协作平台,体验智能研发协作,一起变大变强!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1249312.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

java中的String.format()方法详解

介绍 String.format() 是 Java 中的一个字符串格式化方法&#xff0c;它用于生成指定格式的字符串。这个方法可以接受一个或多个参数&#xff0c;并将它们按照指定的格式插入到字符串中。它使用了类似于 C 语言中的 printf 函数的语法。 String.format() 方法的使用格式如下&…

Linux技能篇-非交互式修改密码

今天的文章没有格式&#xff0c;简单分享一个小技能&#xff0c;就是标题所说–非交互式修改密码。 一、普通方式修改用户密码 最普通的修改密码的命令就是passwd命令 [rootlocalhost ~]# passwd root Changing password for user root. New password: Retype new password:…

【经典小练习】修改文件中的数据

文章目录 &#x1f339;例子&#x1f33a;思路&#x1f6f8;方法一✨报错解决 &#x1f6f8;方法二 &#x1f339;例子 文本文件中有下面的数据 2-1-9-4-7-8 将文件中的数据进行排序&#xff0c;变成下面的数据 1-2-4-7-8-9 &#x1f33a;思路 要对这些数据进行排序&#xf…

发送一个网络数据包的过程解析

在 ip_queue_xmit 中&#xff0c;也即 IP 层的发送函数里面&#xff0c;有三部分逻辑。第一部分&#xff0c;选取路由&#xff0c;也即我要发送这个包应该从哪个网卡出去。 这件事情主要由 ip_route_output_ports 函数完成。接下来的调用链为&#xff1a;ip_route_output_port…

Python报错:AttributeError(类属性、实例属性)

Python报错&#xff1a;AttributeError&#xff08;类属性、实例属性&#xff09; Python报错&#xff1a;AttributeError 这个错误就是说python找不到对应的对象的属性&#xff0c;百度后才发现竟然是初始化类的时候函数名写错了 __init__应该有2条下划线&#xff0c;如果只有…

【JavaEE初阶】Thread 类及常见方法、线程的状态

目录 1、Thread 类及常见方法 1.1 Thread 的常见构造方法 1.2 Thread 的几个常见属性 1.3 启动⼀个线程 - start() 1.4 中断⼀个线程 1.5 等待⼀个线程 - join() 1.6 获取当前线程引用 1.7 休眠当前线程 2、线程的状态 2.1 观察线程的所有状态 2.2 线程状态和状…

黑马点评笔记 分布式锁

文章目录 分布式锁基本原理和实现方式对比Redis分布式锁的实现核心思路实现分布式锁版本一Redis分布式锁误删情况说明解决Redis分布式锁误删问题分布式锁的原子性问题分布式锁-Redission分布式锁-redission可重入锁原理分布式锁-redission锁重试和WatchDog机制分布式锁-redissi…

RocketMQ 消息中间件 知识点汇总

目录 RocketMQ1、什么是RocketMQ?常用术语:2、为什么需要消息队列3、什么是异步处理4、什么是服务解耦5、什么是流量控制6、消息队列两种模型队列模型:发布/订阅模型:总结:7、怎么保证消息不丢失8、如何处理消息被重复消费**出现消息重复的情况:****解决方法:**9、如何保…

RocketMQ消息的一生

这篇文章我准备来聊一聊RocketMQ消息的一生。 不知你是否跟我一样&#xff0c;在使用RocketMQ的时候也有很多的疑惑&#xff1a; 消息是如何发送的&#xff0c;队列是如何选择的&#xff1f; 消息是如何存储的&#xff0c;是如何保证读写的高性能&#xff1f; RocketMQ是如何…

Dreamview底层实现原理

1. Dreamview底层实现原理(3个模块) (1) HMI--可视化人机交互 a. HMIConfig: 1) 支持哪些模式&#xff1b;2)支持哪些地图&#xff1b;3)支持哪些车辆&#xff1b;4)HMIAction HMIMode: b.HMIStatus (2) SimControl (3) Monitor--监视自动驾驶行驶过程中软硬件状态 Referenc…

CentOS 7 安装 Weblogic 14 版本

安装JDK程序 注意&#xff1a;安装weblogic前&#xff0c;先安装JDK&#xff01;&#xff08;要求jdk(1.7以上)&#xff09;&#xff1a; 一、创建用户组weblogic及用户weblogic groupadd weblogic useradd -g weblogic weblogic二、将下载好的jdk及weblogic上传至/home/webl…

2021年12月 Scratch(三级)真题解析#中国电子学会#全国青少年软件编程等级考试

Scratch等级考试(1~4级)全部真题・点这里 一、单选题(共25题,每题2分,共50分) 第1题 执行下列程序,屏幕上可以看到几只小猫? A:1 B:3 C:4 D:0 答案:B 第2题 下列程序哪个可以实现:按下空格键,播放完音乐后说“你好!”2秒? A: B: C:

【Linux】yum -- 软件包管理器

目录 一、Linux中是如何安装软件的 1.1 安装的方法 1.2 安装的本质(基本理解) 二、软件包 2.1 软件包的概念 2.2 为什么要有软件包 三、yum--软件包管理器 3.1 yum的概念 3.2 yum的使用 3.2.1 搜索一个软件 3.2.2 安装一个软件 3.2.3 卸载一个软件 3.3 yum源更新 …

前端vue导出PPT,使用pptxgen.js

前言 公司新需求需要导出ppt给业务用&#xff0c;查阅资料后发现也挺简单的&#xff0c;记录一下。 如有不懂的可以留言&#xff01;&#xff01;&#xff01; 1.安装包 npm install pptxgenjs --save2.引入包 在需要使用的文件中引入 import Pptxgenfrom "pptxgenjs&…

java学习part10 this

90-面向对象(进阶)-关键字this调用属性、方法、构造器_哔哩哔哩_bilibili 1.java的this java的this性质类似cpp的this&#xff0c; 但它是一种引用&#xff0c;所以用 this. xxx来调用。 this代表当前的类的实例&#xff0c;所以必须和某个对象结合起来使用&#xff0c;不能…

rk3588配置uac功能,android13使能uac及adb的复合设备

最近&#xff0c;因新增需求需要在现有产品上增加UAC的功能&#xff0c;查阅并学习相关知识后&#xff0c;在rk3588 SOC硬件平台搭载android13系统平台上成功配置了uac及uac&adb的复合设备。基于开源共享精神希望给大家提供些参考。 1.技术可行性预研 &#xff08;1&#…

nodejs+vue+python+PHP+微信小程序-留学信息查询系统的设计与实现-安卓-计算机毕业设计

1、用户模块&#xff1a; 1&#xff09;登录&#xff1a;用户注册登录账号。 2&#xff09;留学查询模块&#xff1a;查询学校的入学申请条件、申请日期、政策变动等。 3&#xff09;院校排名&#xff1a;查询国外各院校的实力排名。 4&#xff09;测试功能&#xff1a;通过入学…

LabVIEW中将SMU信号连接到PXI背板触发线

LabVIEW中将SMU信号连接到PXI背板触发线 本文介绍如何将信号从PXI&#xff08;e&#xff09;SMU卡路由到PXI&#xff08;e&#xff09;机箱上的背板触发线。该过程涉及使用NI-DCPowerVI将SMU信号导出到PXI_TRIG线上。 在继续操作之前&#xff0c;请确保在开发PC上安装了兼容版…

防止应用程序截屏(容器式,防止极域电子教室和录屏软件录制)

核心原理、实现目的 1、使用Panel容器将外部窗口嵌入自己写的程序 2、使用防止截屏的函数来对窗口透明&#xff0c;这可以使本窗口内所有窗口在录屏软件上消失 3、解放&#xff0c;抓取&#xff0c;存储句柄&#xff0c;实现摆脱录屏&#xff08;极域监控&#xff09; 程序…

Ps:画笔工具的基本操作

画笔工具 Brush Tool是 Ps 中最常用的工具&#xff0c;广泛地用于绘画与修饰工作。 虽然多数操作可在画笔工具的工具选项栏中选择执行&#xff0c;但是如果能记住相应的快捷键可大大提高工作效率。 熟练掌握画笔工具的操作对于使用其他工具也非常有益&#xff0c;因为 Ps 中许多…