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

项目背景

当前的开发环境存在多种不同语言的 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-智能研发协作平台,体验智能研发协作,一起变大变强!

相关推荐

研发团队的「技术债」如何进行量化管理?

技术债或许是所有研发团队都难以避开的课题。面对不可避免的技术债务,我们又该如何科学、恰当地管理和控制它?

LigaAI的博客 1593

技术分享 | SpringBoot 流式输出时,正常输出后为何突然报错?

最近在一个 SpringBoot 项目中遇到了一个很有意思的问题:正常通过过滤器和拦截器的线程变量,在正常输出后突然报错。这是咋回事?

LigaAI的博客 2280

用 MVP(最小可行性产品) 做低成本快速验证,为什么不灵了?| Liga译文

有数据称,九成的初创公司在成立五年内将走向失败。难道号称能快速验证市场的 MVP(最小可行性产品)失效了吗?

LigaAI的博客 1303

技术分享 | 弹窗开发中,如何使用 Hook 封装 el-dialog?

本文将分享如何使用 useDialog Hook 封装 el-dialog,实现更灵活、更易用的弹窗组件。

LigaAI的博客 1508

LigaAI x 极狐GitLab,共探 AI 时代研发提效新范式

LigaAI 和极狐GitLab 达成战略合作,联手打造 AI 赋能的一站式研发效能解决方案,共探 AI 时代研发效能新范式。

LigaAI的博客 1403

精彩回顾 | 「AI 驱动增长,研发数智化升级」分享沙龙成功举办

AI 应用元年,人工智能技术将如何助力企业发展新质生产力,构建增长动能?

LigaAI的博客 847

LigaAI 的 8 个年度关键词 | 2023 年度盘点

一起盘点回顾 2023 年的精彩文章!

LigaAI的博客 1241

我的效率自救之路:对低效的会议说“不!”

如果你也陷入了「会议 - 效率」悖论怪圈,一定不要错过这篇经验分享!

LigaAI的博客 1218

宁波银行:在「金融科技」引擎上,沉浸式提效减负

金融科技企业如何利用有限的研发资源,发挥更高的能效,创造更高的商业价值?

LigaAI的博客 1353

ChatGPT API 调用总超时?破题思路在这 | 技术分享

本文使用 monkey_patch 方式重写 API 参数,解决 ChatGPT API 调用超时问题。

LigaAI的博客 2088

准「AI 时代」下,如何衡量程序员的工作效率和生产力?

都夸 AIGC 好,说 AI 工具可以提高工作效率,但是你真的知道如何判断和衡量 AI 工具对组织的影响吗?

LigaAI的博客 871

SaaS 出海,如何搭建国际化服务体系?(三)

SaaS 企业出海,跨国、跨时区的异步团队如何展开高效的协作与工作?

LigaAI的博客 516

SaaS 出海,如何搭建国际化服务体系?(二)

SaaS 出海潮下,不同规模的企业如何搭建并落地国际化服务团队?

LigaAI的博客 540

SaaS 出海,如何搭建国际化服务体系?(一)

SaaS出海潮下,一个主要由中国人构成的团队在推进海外产品落地的过程中,可能会遇到哪些困难?

LigaAI的博客 619

生成式 AI 如何释放开发者的生产力?

技术管理者有望通过 AIGC 应用,大幅缩短四类关键开发任务的完成时间,进而提升组织生产力。

LigaAI的博客 612

新晋技术管理者如何推动组织变革?

技术管理者通过系统流程、行为和奖励三个重要工具,能够积极推动组织变革,优化团队工作方式,并改善成员评估体系。

LigaAI的博客 419

向上管理:三个技巧,教会你如何与上级、老板高效协作

向上管理是什么?刻板印象中,它总与“职场政治”、“拍马屁”、“奸逆之臣”密不可分,但本文将向你展示向上管理的全新打开方式:领导资源化。

LigaAI的博客 667

如何提高技术领导力?与你分享 5 个心得

「如何培养和提高技术领导力」是开发者成长旅程中最重要的命题之一。无论你想成为技术专家,还是技术管理者,都能从本文获得可复制的实践经验。

LigaAI的博客 1064

产品路线图如何制定?斯坦福大学产品管理课程为你支招

一个合格的产品路线图依赖哪些输入?需要输出哪些信息? 本文将与你分享,笔者在斯坦福大学产品管理课程中习得的路线图管理方法。

LigaAI的博客 533

「程序员转型技术管理」必修的 10 个能力提升方向

本文将为有意走向技术管理的开发者们,提供 10 条具象的成长和转型建议,但别忘了,技术管理转型并不是开发者的唯一出路。

LigaAI的博客 583

产品管理经验分享:删掉 500 个产品待办事项后,我逃离了「假敏捷」

灵魂三问:你的产品待办列表中有多少项工作? 最早的一项在什么时候被创建?列表中有多少维护过但从没进入迭代的待办事项?

LigaAI的博客 336

如何用 NPS 确定研发优先级,打破技术与业务的次元壁?

VUCA 时代下,研发团队应当如何培养、平衡客户视角与商业视角,专注于价值交付,避免陷入「僵尸 Scrum」困境?

LigaAI的博客 402

这 4 个系统可靠性评估指标,可能比 MTTR 更靠谱!

VOID研究报告指出,对复杂的软件系统而言,MTTR 可能不是一个合适的可靠性管理指标。这是怎么回事?

LigaAI的博客 515

LigaAI:从效率、度量和价值维度,成为研发团队的智能医生

「拿不到团队数据」和「无法衡量研发价值」是困扰千万研发团队的难题,LigaAI 将以 AI 为矛,打破定式,以全新的方式赋能研发管理。

LigaAI的博客 442

高绩效团队的 5 个优秀习惯,看看你占了几个?

卓越的团队不一定在才能、技术或者机会上比别的团队更具优势,但是好的工作习惯和工作方式一定会让他们脱颖而出。

LigaAI的博客 351

研发质量指标大 PK:MTTR vs MTBF,谁是靠谱王?

研发管理中,「提高代码/测试质量」更重要,还是「提升故障响应能力」更重要?今天我们从管理指标量化的角度展开解读。

LigaAI的博客 722

介绍 9 个研发质量度量指标

研发质量度量指标中的 MTTR、MTBF、MTTF、MTTD、MTRS 都是什么?今天一次性讲清楚!

LigaAI的博客 1783

3 个技巧,让你像技术专家一样解决编码问题

于开发者而言,比学习语法更重要的,是用代码解决实际问题。本文从技术专家解决问题的技巧出发,提出了三个帮助开发者提升复杂问题解决能力的方法。

LigaAI的博客 904

ChatGPT 之后,B 端产品设计会迎来颠覆式革命吗?| Liga妙谈

ChatGPT 掀起了国内的大模型革命,当一部分先行者正在努力探索中国 LLM 的形态,LigaAI 关心起了 GPT 类人工智能在 B 端产品的落地与影响。

LigaAI的博客 1691

技术分享 | 如何编写同时兼容 Vue2 和 Vue3 的代码?

资深前端倾情分享:如何实现同时兼容 Vue2 与 Vue3 的代码?业务实践中有哪些代码精简和优化的小技巧?

LigaAI的博客 1636
上一篇: 准「AI 时代」下,如何衡量程序员的工作效率和生产力?
下一篇: ChatGPT API 调用总超时?破题思路在这 | 技术分享
LigaAI
LigaAI 企业官方账号 企业官方账号
博客等级 码龄6年 258粉丝 85原创
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值