項目背景
當前的開發環境存在多種不同語言的 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 的過程。
- 在 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, "");
}
- 加載對應的 URL,渲染頁面。
public static void loadURL(JBCefBrowser browser, String url) {//如果不需要設置和瀏覽器顯示相關的,可忽略browser.getJBCefClient().addDisplayHandler(settingsDisplayHandler, browser.getCefBrowser());browser.loadURL(url);
}
- 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}}
}
- 前端發送請求調用插件,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 {@Overridepublic 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;}
}
- 插件前端代碼。
// 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 和瀏覽器之間的交互路由名稱不能設置為 cefQuery
和 cefQueryCancel
。
這兩個為 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];// 這里先獲取目標文件,轉成虛擬文件,再獲取對應URLURL 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 {@Overridepublic 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-智能研發協作平臺,體驗智能研發協作,一起變大變強!