IntelliJ IDE 插件开发 | (十四)使用 JCEF 嵌入网页

系列文章

本系列文章已收录到专栏,交流群号:689220994,也可点击链接加入,如果想加入微信群可以加我个人微信(butterfly__1221)拉你进群。

前言

JCEF(Java Chromium Embedded Framework)是 CEF 针对 Java 的一个版本,通过使用 JCEF 可以在应用程序中嵌入网页,而 IDEA 插件开发也从 2020.1 版本开始支持这一特性,在此之前则是通过 JavaFX 去实现嵌入网页。对于不熟悉 Swing UI 、Kotlin UI DSL 等 GUI 工具的小伙伴来说,在开发复杂 UI 界面的时候就可以考虑使用 JCEF 去实现(官方还是不建议),本文会通过一系列的示例讲解 JCEF 的用法,涉及到的完整代码也已上传到GitHub

初始化

为了便于演示,后续演示均在侧边栏中展示效果,如果不熟悉侧边栏的创建,可以查看本专栏的第二篇文章。下面以一个Hello, world!的示例开始本文:

class JCEFSidebarConfig: ToolWindowFactory {

    override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
        // ①
        val browser = JBCefBrowser()
        browser.loadHTML("<h1 style=\"color: red;\">Hello World!</h1>")
        // ②
        toolWindow.contentManager.addContent(
            ContentFactory.getInstance().createContent(browser.component, "", false))
    }

}

其中①用于初始化JBCefBrowser对象,然后直接使用loadHTML加载 HTML 文本(还可以加载本地 HTML 文件或者外部链接),之后在②中将 browser 的 UI 组件添加到侧边栏中即可实现下图所示的效果:

image-20250108171815241

为了减少不必要的代码展示,后续在createToolWindowContent中处理的部分,只展示①部分的代码。

加载网页的几种方式

直接加载 HTML 内容

可以直接通过loadHTML(String html)加载 HTML 文本,例如上述的 demo。该方法还有loadHTML(String html, String url)两个参数的版本,loadHTML(String html)其实j就是loadHTML(html, "about:blank")的简写,第二个参数 url 用于设置基础 url,比如可以设置为 D 盘位置,然后 HTML 设置图片的相对路径即可:

browser.loadHTML("<img src=\"img/test.jpg\"/>", "file:///D:")

image-20250120125803917

这种方式只作了解即可,对于稍复杂界面还是会使用下面介绍的loadURL方法。

通过 URL 加载页面

加载网页地址

通过loadURL方法可以直接加载网页地址,例如展示百度首页:

browser.loadURL("https://www.baidu.com")

image-20250120160343349

加载本地文件

此外也可以选择加载本地的网页文件,但是稍微复杂些,步骤如下:

  1. 注册自定义处理的协议、域名和处理方法,然后再加载:

    CefApp.getInstance().registerSchemeHandlerFactory("http", "butterfly") { _, _, _, _ -> JCEFResourceHandler() }
    browser.loadURL("http://butterfly/index.html")
    

    这里的butterfly用于标识自定义的域名,用于后续映射本地文件地址。

  2. 自定义处理器,拦截自定义域名映射到本地文件:

    class JCEFResourceHandler : CefResourceHandler {
    
        private var connection: JCEFConnection? = null
    
        override fun processRequest(cefRequest: CefRequest, callback: CefCallback): Boolean {
            val url = cefRequest.url
            // 拦截本地文件请求并处理
            if (url.startsWith("http://butterfly")) {
                val resource = javaClass.classLoader.getResource(url.replace("http://butterfly", "static")) ?: return false
                connection = JCEFConnection(resource.toURI().toURL().openConnection())
                callback.Continue()
                return true
            }
            return false
        }
    
        override fun getResponseHeaders(cefResponse: CefResponse, responseLength: IntRef, redirectUrl: StringRef?) {
            connection?.getResponseHeaders(cefResponse, responseLength)
        }
    
        override fun readResponse(dataOut: ByteArray, dataSize: Int, bytesRead: IntRef, callback: CefCallback?): Boolean {
            return connection?.readResponse(dataOut, dataSize, bytesRead) ?: false
        }
    
        override fun cancel() {
            connection?.close()
        }
    
    }
    

    这里的static就是存放本地文件的目录:

    image-20250121151200288

    JCEFConnection用于处理返回信息,下面会进行展示。

  3. 处理返回信息:

    class JCEFConnection(private var connection: URLConnection) {
    
        private var inputStream = connection.getInputStream()
    
        fun getResponseHeaders(cefResponse: CefResponse, responseLength: IntRef) {
            // 设置响应头信息
            cefResponse.mimeType = connection.contentType
            responseLength.set(inputStream.available())
            cefResponse.status = 200
        }
    
        fun readResponse(dataOut: ByteArray, dataSize: Int, bytesRead: IntRef): Boolean {
            // 读取响应数据
            inputStream.use {
                val available = it.available()
                if (available > 0) {
                    bytesRead.set(it.read(dataOut, 0, available.coerceAtMost(dataSize)))
                    return true
                }
            }
            return false
        }
    
        fun close() {
            // 关闭流
            inputStream.close()
        }
    
    }
    
  4. 经过以上设置后就可以展示本地文件,效果如下:

    image-20250121152343666

    image-20250121152333749

事件处理器

JCEF 提供了一系列的事件处理器用于处理浏览器触发的各种事件,例如可以自定义加载事件:

browser.jbCefClient.addLoadHandler(object : CefLoadHandler {
    override fun onLoadingStateChange(p0: CefBrowser?, p1: Boolean, p2: Boolean, p3: Boolean) {
        println("加载状态变更")
    }
    override fun onLoadStart(p0: CefBrowser?, p1: CefFrame?, p2: CefRequest.TransitionType?) {
        println("加载开始")
    }
    override fun onLoadEnd(p0: CefBrowser?, p1: CefFrame?, p2: Int) {
        println("加载结束")
    }
    override fun onLoadError(
        p0: CefBrowser?,
        p1: CefFrame?,
        p2: CefLoadHandler.ErrorCode?,
        p3: String?,
        p4: String?
    ) {
        println("加载出错")
    }
}, browser.cefBrowser)

image-20250121154122790

也可以自定义文件下载事件:

browser.jbCefClient.addDownloadHandler(object : CefDownloadHandler {
    override fun onBeforeDownload(
        browser: CefBrowser?,
        downloadItem: CefDownloadItem?,
        suggestedName: String?,
        callback: CefBeforeDownloadCallback
    ) {
        callback.Continue("", true)
    }
    override fun onDownloadUpdated(browser: CefBrowser?, downloadItem: CefDownloadItem?, callback: CefDownloadItemCallback?) {
        println("下载状态更新")
    }
}, browser.cefBrowser)

还可以自定义 JS 弹框事件,使用 IDE 的弹框展示前端弹框要展示的信息:

browser.jbCefClient.addJSDialogHandler(object : CefJSDialogHandler {
    override fun onJSDialog(
        browser: CefBrowser?,
        originUrl: String?,
        dialogType: CefJSDialogHandler.JSDialogType?,
        messageText: String?,
        defaultPromptText: String?,
        callback: CefJSDialogCallback,
        suppressMessage: BoolRef?
    ): Boolean {
        invokeLater {
            Messages.showInfoMessage(messageText, "")
            callback.Continue(true, messageText)
        }
        return true
    }
    override fun onBeforeUnloadDialog(p0: CefBrowser?, p1: String?, p2: Boolean, p3: CefJSDialogCallback?) = true
    override fun onResetDialogState(p0: CefBrowser?) {}
    override fun onDialogClosed(p0: CefBrowser?) {}
}, browser.cefBrowser)

除了以上三个事件外,JCEF 还提供很多其它方面的事件,这里就不再一一展示,可以在GitHub中查看所有支持的事件。

插件代码与 JS 互操作

如果想要实现插件代码与 JS 互操作,可以使用 JCEF 自带的CefMessageRouter来处理,下面会介绍如何借助CefMessageRouter来实现两者的相互调用,其实也可以选择使用Websocket进行交互,这里就不再介绍。

  1. 创建并设置消息路由:

    // ①
    val routerConfig = CefMessageRouter.CefMessageRouterConfig("javaQuery", "javaQueryCancel")
    val messageRouter = CefMessageRouter.create(routerConfig, object : CefMessageRouterHandlerAdapter() {
        
        override fun onQuery(
            browser: CefBrowser,
            frame: CefFrame?,
            queryId: Long,
            request: String?,
            persistent: Boolean,
            callback: CefQueryCallback
        ): Boolean {
            // ②
            if (request == "butterfly") {
                invokeLater {
                    println("后端收到请求")
                    browser.executeJavaScript("window.butterfly('Hello, world!')", null, 0)
                }
                callback.success("butterfly")
                return true
            }
            return false
        }
    })
    browser.jbCefClient.cefClient.addMessageRouter(messageRouter)
    

    上述代码中的①是进行消息路由设置,提供给前端调用的方法名,注意这里的名称不能使用内部已经使用的cefQuerycefQueryCancel

    image-20250122090140977

    上述代码中的②用于处理前端请求,包括使用executeJavaScript直接执行 JS 脚本代码和通过回调返回信息。

  2. 前端代码处理:

    首先是用于发起请求的代码:

    <button onclick="sendMessage()">发送消息</button>
    <script>
        function sendMessage() {
            window.javaQuery({
                request: 'butterfly',
                persistent: false,
                onSuccess: function(response) {
                    console.log(`前端收到响应消息:${response}`)
                },
                onFailure: function(error_code, error_message) {}
            })
        }
    </script>
    

    然后是提供给插件代码调用的函数:

    window.butterfly = function (msg) {
        console.log(`前端收到消息${msg}`)
    }
    
  3. 最终效果如下:

    动画

统一滚动条样式

使用 JCEF 时默认的滚动条样式和浏览器中的一致,如下所示:

image-20250122124200526

可以看到滚动条的样式和 IDE 的样式格格不入,当然我们也可以通过 CSS 自定义滚动条的样式。不过,官方已经为我们考虑好了,JBCefScrollbarsHelper.buildScrollbarsStyle()方法会返回和 IDE 风格一致的滚动条样式,然后结合前文中的消息路由,就可以通过回调返回到前端进行设置:

if (request == "init") {
    callback.success(JBCefScrollbarsHelper.buildScrollbarsStyle())
}

window.javaQuery({
    request: 'init',
    persistent: false,
    onSuccess: function(scrollbarStyle) {
        const style = document.createElement('style')
        style.innerHTML = scrollbarStyle
        document.head.appendChild(style)
    },
    onFailure: function(error_code, error_message) {}
})

最终效果如下:

image-20250122124603932

调试

如果想要对嵌入的网页进行调试,则可以通过点击顶部菜单的Help | Find Action,然后输入ide.browser.jcef.debug.port,双击后可以修改端口(这里的 12345 是我自定义的):

image-20250122141435105

然后在谷歌浏览器中输入chrome://inspect/#devices打开调试工具:

image-20250122141731510

点击inspect即可进入调试界面:

image-20250122141755685

总结

本文讲解了如何在 IDEA 插件开发中使用 JCEF 在应用中嵌套网页,对于大部分开发场景以上内容已经足够了,如果本文有不足之处,也欢迎评论进行交流讨论。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值