系列文章
本系列文章已收录到专栏,交流群号: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 组件添加到侧边栏中即可实现下图所示的效果:
为了减少不必要的代码展示,后续在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:")
这种方式只作了解即可,对于稍复杂界面还是会使用下面介绍的loadURL
方法。
通过 URL 加载页面
加载网页地址
通过loadURL
方法可以直接加载网页地址,例如展示百度首页:
browser.loadURL("https://www.baidu.com")
加载本地文件
此外也可以选择加载本地的网页文件,但是稍微复杂些,步骤如下:
-
注册自定义处理的协议、域名和处理方法,然后再加载:
CefApp.getInstance().registerSchemeHandlerFactory("http", "butterfly") { _, _, _, _ -> JCEFResourceHandler() } browser.loadURL("http://butterfly/index.html")
这里的
butterfly
用于标识自定义的域名,用于后续映射本地文件地址。 -
自定义处理器,拦截自定义域名映射到本地文件:
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
就是存放本地文件的目录:JCEFConnection
用于处理返回信息,下面会进行展示。 -
处理返回信息:
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() } }
-
经过以上设置后就可以展示本地文件,效果如下:
事件处理器
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)
也可以自定义文件下载事件:
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
进行交互,这里就不再介绍。
-
创建并设置消息路由:
// ① 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)
上述代码中的①是进行消息路由设置,提供给前端调用的方法名,注意这里的名称不能使用内部已经使用的
cefQuery
和cefQueryCancel
。上述代码中的②用于处理前端请求,包括使用
executeJavaScript
直接执行 JS 脚本代码和通过回调返回信息。 -
前端代码处理:
首先是用于发起请求的代码:
<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}`) }
-
最终效果如下:
统一滚动条样式
使用 JCEF 时默认的滚动条样式和浏览器中的一致,如下所示:
可以看到滚动条的样式和 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) {}
})
最终效果如下:
调试
如果想要对嵌入的网页进行调试,则可以通过点击顶部菜单的Help | Find Action
,然后输入ide.browser.jcef.debug.port
,双击后可以修改端口(这里的 12345 是我自定义的):
然后在谷歌浏览器中输入chrome://inspect/#devices
打开调试工具:
点击inspect
即可进入调试界面:
总结
本文讲解了如何在 IDEA 插件开发中使用 JCEF 在应用中嵌套网页,对于大部分开发场景以上内容已经足够了,如果本文有不足之处,也欢迎评论进行交流讨论。