
簡介JxBrowser 7.19 是面向 Java 桌面應用開發(fā)者的嵌入式瀏覽器組件資源適合在 Swing、SWT、JavaFX 等界面中集成網(wǎng)頁渲染、HTML 交互、登錄授權與跨平臺瀏覽能力的項目使用也可為舊版本升級、兼容性驗證和多端部署提供參考。壓縮包共 1359 個文件以 1345 個 HTML 文檔為主輔以 10 個 JAR 運行庫及少量腳本、樣式與索引文件整體約 437.21MB內(nèi)容覆蓋 Windows、Linux、macOS 等桌面與 ARM 平臺依賴并附帶完整 Javadoc 與 Browser.java 示例方便按容器類型選擇依賴并完成接入。已有 2599 人學習下載。讀者可獲得全平臺運行庫、較完整的接口文檔和最小接入示例用于核對版本差異、搭建開發(fā)環(huán)境、驗證容器嵌入方式并對照平臺分包排查常見集成問題文檔目錄也便于按類名與接口快速檢索減少反復試錯對需要維護多端一致體驗的團隊較為實用并降低環(huán)境配置成本。1. 用 jxbrowser java 把 Chromium 塞進 Swing它真正解決的是哪一類問題Java 桌面端要嵌一個現(xiàn)代內(nèi)核的瀏覽器這事兒的坑比想象中深。JavaFX 自帶的 WebView 引擎版本老H5 報表里的 ES6 語法一上來就白屏JCEF 能跑但編譯鏈得自己維護換個 JDK 就得重來一遍。所以我一般會把 jxbrowser java 這條路線重新?lián)炱饋怼狫xBrowser 7.19 屬于 7.x 線上比較穩(wěn)的一個點本質(zhì)是把整個 Chromium 內(nèi)核封裝成Engine/Browser兩層 JVM 對象Swing、JavaFX、SWT 都能掛上去。它值錢的地方不是“能顯示網(wǎng)頁”而是網(wǎng)頁里的 JS 能回調(diào) Java、Java 能攔請求改響應、離屏模式下還能截圖和打印。適合誰做桌面客戶端、界面里要嵌 H5 模塊、又不想碰 CEF 編譯鏈的 Java 團隊。它是商業(yè)庫這點要有心理準備。2. 依賴坐標與內(nèi)核目錄jxbrowser 7.19 的包結(jié)構(gòu)拆開看把 7.19 的構(gòu)件下下來第一反應往往是“怎么這么多 jar”。它不是一個單體包而是按宿主 UI 框架切的核心 API 一個 jarSwing / JavaFX / SWT 各自的視圖實現(xiàn)各一個 jar再疊一個裝 Chromium 原生庫的 classifier 包。搞不清這個分層最容易出的問題就是“代碼編譯過了一跑就NoClassDefFoundError”或者打出來的安裝包莫名其妙 800 MB。2.1 四個構(gòu)件各管什么別一股腦全引進來先看分工這張表建議存下來排查依賴沖突時對著看構(gòu)件里面是什么什么時候引jxbrowserEngine、Browser、Frame、Network等核心 API必引jxbrowser-swingBrowserView的 Swing 實現(xiàn)界面是 Swing / AWTjxbrowser-javafxBrowserView的 JavaFX 實現(xiàn)界面是 JavaFXjxbrowser-swtSWT 宿主適配老 RCP 項目核心包只提供“引擎 瀏覽器實例 DOM 操作 網(wǎng)絡事件”它不知道你要把畫面貼到哪個組件上。視圖包才是把渲染結(jié)果落到JComponent或Node上的那一層。所以只引jxbrowser不引jxbrowser-swingBrowserView.newInstance(browser)會直接找不到類。真正的體積大頭在 classifier 上。7.x 的坐標長這樣!-- 核心 API必引 -- dependency groupIdcom.teamdev.jxbrowser/groupId artifactIdjxbrowser/artifactId version7.19/version !-- cross-desktop 把 win64/linux64/mac64/mac64arm 的原生庫全塞進一個 jar 開發(fā)期省事發(fā)布期建議換成單平臺 classifier -- classifiercross-desktop/classifier /dependency !-- Swing 宿主視圖 -- dependency groupIdcom.teamdev.jxbrowser/groupId artifactIdjxbrowser-swing/artifactId version7.19/version /dependencyGradle 寫法等價dependencies { implementation(com.teamdev.jxbrowser:jxbrowser:7.19:cross-desktop) implementation(com.teamdev.jxbrowser:jxbrowser-swing:7.19) }這里的關鍵參數(shù)是classifier。cross-desktop是“全平臺 fat jar”好處是同一份依賴在 Windows 開發(fā)機、Linux 構(gòu)建機、Mac 上都能直接跑壞處是每個平臺的chrome可執(zhí)行文件和.dll/.so/.dylib都在里面包體直接翻幾倍。發(fā)布階段我一般會改成單平臺 classifier——win64、linux64、mac64、mac64arm這幾個按目標平臺分別構(gòu)建安裝包能瘦一大圈。JDK 版本上7.x 的編譯目標是 Java 8所以 8、11、17 都能跑我這邊的 11 和 17 環(huán)境都驗證過。至于 21 及以上得看構(gòu)件是否帶了對應的模塊化描述動手前先寫個Engine.newInstance的 ten 行 demo 試一下比看文檔快。2.2 渲染模式選型HARDWARE_ACCELERATED 和 OFF_SCREEN 差在哪這一步選錯后面所有界面問題都是它引起的。7.x 的RenderingMode實際上就兩條路模式渲染路徑優(yōu)點代價HARDWARE_ACCELERATED內(nèi)核開原生窗口直接疊在宿主窗口上性能接近獨立瀏覽器視頻、Canvas 都流暢z-order 由操作系統(tǒng)管Swing 的彈窗、JPopupMenu會被瀏覽器視圖蓋住OFF_SCREEN渲染到離屏緩沖再合成到 Swing 組件上能被 Swing 正常遮擋、能截圖、能透明疊加必須手動resize滾動和輸入事件得自己轉(zhuǎn)發(fā)滾動幀率明顯低一檔判斷標準很樸素如果你的界面里瀏覽器區(qū)域是“一整塊不跟別的組件重疊的矩形”選HARDWARE_ACCELERATED如果需要在這個區(qū)域上浮一層自定義的 Swing 控件、或者要對著頁面做截圖和錄像、或者要嵌進帶透明背景的面板里選OFF_SCREEN。OFF_SCREEN有一個幾乎人人都會踩的點不調(diào)resize就是一片白。它不是自動跟隨容器尺寸的必須顯式告訴內(nèi)核畫布多大// 離屏模式下必須先給出畫布尺寸否則內(nèi)核不知道渲染多大表現(xiàn)為純白 browser.resize(1280, 800); BrowserView view BrowserView.newInstance(browser); // 組件尺寸變化時要同步回去否則拉伸窗口后頁面還是舊尺寸 view.addComponentListener(new ComponentAdapter() { Override public void componentResized(ComponentEvent e) { browser.resize(Math.max(1, view.getWidth()), Math.max(1, view.getHeight())); } });resize的寬高必須大于 0最小化窗口時如果傳 0內(nèi)核那邊會拋參數(shù)異常所以上面加了Math.max(1, ...)兜底。這一行是血淚經(jīng)驗不加的話用戶一最小化再還原就崩。2.3 內(nèi)核落地目錄別讓首次啟動去解壓臨時目錄Engine.newInstance第一次執(zhí)行時會把 Chromium 內(nèi)核解壓出來。默認落到系統(tǒng)臨時目錄下的一個帶版本號的子目錄里。開發(fā)機上無所謂生產(chǎn)環(huán)境就有兩個隱患一是臨時目錄可能被安全策略限制寫入二是某些清理工具會把臨時目錄當垃圾掃掉用戶第二天打開軟件就報內(nèi)核加載失敗。所以我一般會顯式指定內(nèi)核目錄Path base Paths.get(System.getProperty(user.home), .myapp); Path chromiumDir base.resolve(chromium); EngineOptions options EngineOptions.newBuilder(RenderingMode.HARDWARE_ACCELERATED) // license 單獨封裝別硬編碼在源碼里也別提交到倉庫 .licenseKey(LicenseStore.load()) // 內(nèi)核落地目錄要求可寫預留至少 500MB 空間 .chromiumDir(chromiumDir) // 虛擬機和遠程桌面環(huán)境下關掉著色器緩存能少一類花屏問題 .addSwitch(--disable-gpu-shader-disk-cache) .build(); Engine engine Engine.newInstance(options);三個參數(shù)逐個說。licenseKey是必填的7.x 沒有合法 license 會在newInstance階段直接拋異常不是運行到一半才攔。chromiumDir指定解壓目標一旦指定后續(xù)啟動會復用這份內(nèi)核不再重復解壓冷啟動能從幾十秒降到幾百毫秒。addSwitch是往 Chromium 命令行透傳開關等價于你在啟動 chrome 時加參數(shù)排障時特別有用——花屏加--disable-gpu字體渲染異常試--disable-font-subpixel-positioning。如果chromiumDir這個構(gòu)建器方法報“找不到符號”先別懷疑自己寫錯了多半是拿到的構(gòu)件不是標準 7.19 的官方構(gòu)建被重新打包過。版本對不上的包后面還會在內(nèi)核版本、license 校驗上出各種玄學問題。指定完目錄要驗證進到~/.myapp/chromium下應該能看到一個chromeWindows 上是chrome.exe加一堆動態(tài)庫總大小幾百 MB。如果目錄是空的說明參數(shù)沒生效或者寫入被攔了去看日志里有沒有被吞掉的IOException。3. 最小閉環(huán)Engine 單例、Browser 多開、BrowserView 掛載依賴理順了接下來是把一個能跑的窗口搭出來。這三層對象的生命周期差別很大混著用是后面卡頓和進程泄漏的根源Engine是重對象一個進程只能有一個Browser是輕對象可以開很多個BrowserView只是把某個Browser的畫面貼到某個組件上不持有內(nèi)核狀態(tài)。3.1 一個線程安全的 Engine 持有類Engine.newInstance是阻塞的而且內(nèi)部會拉起一整套 Chromium 子進程。放在 EDT 上執(zhí)行界面會直接凍住十幾秒——這不是性能問題是“用戶以為程序掛了”的問題。所以我把引擎初始化單獨抽出來public final class BrowserEngineHolder { private static volatile Engine instance; public static Engine get() { Engine local instance; if (local null) { synchronized (BrowserEngineHolder.class) { local instance; if (local null) { EngineOptions options EngineOptions.newBuilder(RenderingMode.HARDWARE_ACCELERATED) .licenseKey(LicenseStore.load()) .chromiumDir(Paths.get(System.getProperty(user.home), .myapp, chromium)) .build(); local Engine.newInstance(options); instance local; // 兜底關閉防止用戶直接關進程留下僵尸子進程 Runtime.getRuntime().addShutdownHook(new Thread(local::close, jxbrowser-shutdown)); } } } return local; } private BrowserEngineHolder() { } }雙檢鎖是為了保證并發(fā)調(diào)用時只創(chuàng)建一個引擎。很多項目啟動時會并發(fā)初始化多個業(yè)務面板如果每個面板都newInstance你會看到任務管理器里瞬間冒出幾組 Chromium 進程每個都占幾百 MB還互相搶 GPU 進程。addShutdownHook那一行是后悔藥。用戶在任務管理器里結(jié)束進程時JVM 的關閉鉤子仍會執(zhí)行能把這批 Chromium 子進程一起收掉沒有它異常退出后下次啟動可能出現(xiàn)端口/鎖文件沖突。這個get()必須在啟動階段、非 EDT 線程上預熱一次。我一般放在 splash 界面加載時用SwingWorker里跑界面先顯示“正在初始化瀏覽器內(nèi)核…”。3.2 Browser 的創(chuàng)建與 URL 加載時機Browser很便宜engine.newBrowser()通常在幾十毫秒內(nèi)返回。真正耗時的是頁面導航。所以創(chuàng)建和加載要分開看Engine engine BrowserEngineHolder.get(); Browser browser engine.newBrowser(); BrowserView view BrowserView.newInstance(browser); panel.setLayout(new BorderLayout()); panel.add(view, BorderLayout.CENTER); // 導航是異步的這里返回不代表頁面已經(jīng)加載完 browser.navigation().loadUrl(https://report.internal/dashboard); // 加載完成事件里再做 DOM 注入、JS 通信這類事 browser.navigation().on(FinishLoading.class, event - { LOG.info(loaded: {}, event.url()); });loadUrl是異步的。緊接著在下一行去executeJavaScript經(jīng)常拿到的是null或者空對象因為 DOM 還沒建完。所有依賴頁面結(jié)構(gòu)的操作都得掛在FinishLoading或DocumentLoaded回調(diào)里。同一個Browser可以重復用切換頁面用loadUrl或loadHtml不需要關掉重建。但換業(yè)務模塊時建議browser.close()再新建一個——不是必須只是把可能泄漏的 JS 定時器和事件監(jiān)聽一起清掉。BrowserView.newInstance(browser)這個方法名在HARDWARE_ACCELERATED和OFF_SCREEN下是同一個差異在Engine那邊就定了。所以渲染模式必須在建引擎時就定死中途改不了。3.3 關閉順序先 browser 后 engine關閉順序錯了控制臺會刷一堆IllegalStateException嚴重的時候進程退不干凈。正確順序是自下而上// 關閉單個瀏覽器視圖 browser.close(); // 應用退出時再關引擎順序不能反 engine.close();browser.close()會釋放這個頁面的渲染進程和所有 JS 上下文engine.close()會終止整個 Chromium 進程組。如果先engine.close()再browser.close()后者會對一個已經(jīng)失效的引擎發(fā)指令拋異常如果只關窗口不close瀏覽器渲染進程會一直掛著用戶反復打開關閉同一個模塊進程數(shù)會一路漲上去。一個常見的錯誤寫法是在windowClosing里先調(diào)engine.close()再dispose()界面。改成先關所有Browser最后統(tǒng)一關Engine這類問題就沒了。4. 雙向通信JS 調(diào) Java、Java 注 JS、請求攔截頁面能顯示只是第一步。真正讓嵌入方案有價值的是雙向通信網(wǎng)頁里的按鈕要觸發(fā) Java 側(cè)的下載、Java 側(cè)要把登錄態(tài)塞給頁面、某些請求要在網(wǎng)絡層被改寫。JxBrowser 7.x 給了三層機制從輕到重依次是注入執(zhí)行、對象橋接、網(wǎng)絡攔截。4.1 JsAccessible 加 JsObject把 Java 對象掛到 window 上最省事的做法是把一個普通 Java 對象掛到window上頁面里直接當 JS 對象調(diào)用。前提是方法必須標JsAccessible不標的一律不可見——這既是安全邊界也避免把整個對象圖暴露出去public final class HostBridge { JsAccessible public String readToken() { return TokenStore.current(); } JsAccessible public void log(String message) { LOG.info([web] {}, message); } }掛載的時機必須等頁面加載完browser.navigation().on(FinishLoading.class, event - { browser.mainFrame().ifPresent(frame - { // executeJavaScript 返回 JsObject 時是頁面里的 window 本身 JsObject window frame.executeJavaScript(window); // 掛上去之后頁面里可以直接 hostBridge.readToken() window.putProperty(hostBridge, new HostBridge()); }); });frame.executeJavaScript(window)這里看起來有點繞但它返回的就是頁面window的JsObject引用putProperty相當于在頁面里執(zhí)行window.hostBridge {...}。參數(shù)名這里是hostBridge就是頁面?zhèn)仍L問用的全局變量名別跟業(yè)務里已有的變量撞。注意兩點。第一JsAccessible方法是在哪個線程被調(diào)用的取決于內(nèi)核回調(diào)線程不是 EDT方法體里如果要改 Swing 組件必須SwingUtilities.invokeLater包一層否則就是經(jīng)典的隨機崩潰。第二參數(shù)和返回值只支持基礎類型和字符串傳自定義對象會靜默變成undefined這個坑很隱蔽。4.2 executeJavaScript 的返回值邊界Promise 不是值executeJavaScript看著像個萬能入口實際上它只能同步拿到表達式的即時結(jié)果。頁面里fetch(...)、async函數(shù)返回的是一個 Promise 對象你拿到的是JsObject不是數(shù)據(jù)// 錯誤示范拿到的不是數(shù)據(jù)是 Promise 的包裝對象 Object wrong frame.executeJavaScript(fetch(/api/profile).then(r r.json())); // 可行做法讓頁面同步返回序列化好的字符串 String json frame.executeJavaScript(JSON.stringify(window.__profile || null));所以我的習慣是不指望從executeJavaScript直接取異步結(jié)果而是在頁面?zhèn)葤煲粋€回調(diào)函數(shù)Java 側(cè)先注冊橋接對象頁面拿到數(shù)據(jù)后主動調(diào)hostBridge.onProfileLoaded(json)。這樣數(shù)據(jù)流向是單向的、可追蹤的出問題也容易定位。JSON.stringify那一步別省。JxBrowser 對 JS 對象的自動轉(zhuǎn)換只覆蓋基礎類型直接返回Object或者數(shù)組經(jīng)常得到一個看起來像數(shù)組、實際取不到值的怪東西也不好斷點。序列化成字符串Java 側(cè)用任意 JSON 庫解析行為最確定。4.3 網(wǎng)絡攔截BeforeUrlRequest 攔請求、BeforeSendHeaders 改頭網(wǎng)絡層是這套方案里最容易被低估的能力。engine.network()上可以訂閱一系列事件用來做請求阻斷、請求頭注入、mock 響應// 阻斷某個路徑下的所有請求常用于屏蔽頁面里的第三方統(tǒng)計腳本 engine.network().on(BeforeUrlRequest.class, event - { if (event.url().contains(/legacy/)) { event.preventDefault(); // 攔掉頁面?zhèn)缺憩F(xiàn)為請求失敗 } }); // 給所有出站請求補一個客戶端標識后端按這個標識下發(fā)不同版本 engine.network().on(BeforeSendHeaders.class, event - { HttpHeaders headers event.headers().toBuilder() .add(X-Client, desktop/1.0) .build(); event.setHeaders(headers); });BeforeUrlRequest觸發(fā)在請求發(fā)出之前preventDefault()就是阻斷頁面里對應的fetch會走 reject 分支所以頁面?zhèn)纫卸档滋幚矸駝t用戶看到的是一個一直轉(zhuǎn)圈的加載態(tài)。BeforeSendHeaders觸發(fā)在請求頭組裝完成之后headers().toBuilder()拷一份再改不要直接改原對象。這兩個接口在不同小版本之間簽名略有出入寫的時候靠 IDE 補全確認參數(shù)類型別照抄網(wǎng)上的老代碼。另外網(wǎng)絡事件回調(diào)不保證在 EDT 上回調(diào)體里做重活比如寫數(shù)據(jù)庫、讀大文件會拖慢整個網(wǎng)絡層建議只做判斷和轉(zhuǎn)發(fā)。5. 避坑與排查許可證、首次啟動、EDT、進程殘留、打包五個翻車點前面四章都是“怎么跑起來”這一章講“為什么跑不起來”。下面五個問題是我在不同項目里反復遇到的按出現(xiàn)頻率排。每個都按現(xiàn)象、原因、解決三段寫遇到了直接對號入座。5.1 許可證與內(nèi)核來源兩個最容易被忽略的前置條件現(xiàn)象一本地開發(fā)一切正常換到測試機或者打包后運行啟動即拋異常提示 license 缺失或無效。原因7.x 的 license 校驗發(fā)生在Engine.newInstance而且它是跟簽發(fā)時的應用標識綁定的不是隨便一個字符串都能過。另外把 license 硬編碼進源碼、或者放在沒被打進安裝包的資源目錄里都會導致運行期讀不到。解決把 license 的讀取抽成單獨一層優(yōu)先讀外部配置文件其次讀環(huán)境變量最后才回落到打包內(nèi)的資源。啟動時把 key 的前 8 位打進日志不要打全量一眼就能看出讀到的是不是預期那一份。生產(chǎn)環(huán)境更穩(wěn)的做法是把 license 放在安裝目錄的conf/下隨配置一起下發(fā)方便替換。現(xiàn)象二chromiumDir指定了目錄卻始終是空的或者目錄里有文件但程序報內(nèi)核版本不匹配。原因拿到的構(gòu)件被重新打包過classifier 里的原生庫和jxbrowser核心包的版本對不上。這種情況在來源不明的整合包里特別常見。解決核對 jar 的清單文件里的版本號和依賴聲明里的7.19是否一致。對不上就換回標準構(gòu)件。生產(chǎn)項目不要用來源不明的整合包出了問題沒有任何線索可查純屬給自己埋雷。5.2 線程與卡死在 EDT 上碰 Engine 的代價現(xiàn)象雙擊圖標后窗口白屏十幾秒到一分鐘進度條不動Windows 直接標“未響應”。原因Engine.newInstance在 EDT 上執(zhí)行。首次運行要解壓幾百 MB 的內(nèi)核文件這段時間 EDT 被完全占住Swing 連重繪都做不了。解決把引擎初始化放到啟動線程或者SwingWorker#doInBackground里界面先顯示一個 splash 或者“初始化中”的占位面板初始化完成后再切到主界面。第二次及以后的啟動因為內(nèi)核已解壓通常幾百毫秒就夠但代碼不能假設“一定是第二次”。現(xiàn)象點擊頁面按鈕后整個界面卡住幾秒然后恢復日志里能看到 JS 調(diào)用記錄。原因在 EDT 上同步等待executeJavaScript的結(jié)果或者JsAccessible方法體里做了阻塞 IO。解決橋接方法里只做輕量轉(zhuǎn)換把耗時邏輯丟到線程池需要的返回值通過回調(diào)再送回頁面。判斷標準很簡單——橋接方法體超過 10 行就該拆了。5.3 進程與打包任務管理器里的進程數(shù)是最好的體檢指標現(xiàn)象反復打開關閉同一個業(yè)務模塊任務管理器里 Chromium 相關進程數(shù)一路往上漲內(nèi)存也跟著漲最后 OOM。原因只把BrowserView從容器里remove了沒有browser.close()。視圖沒了但渲染進程和 JS 上下文還活著。解決把browser.close()和界面銷毀綁在一起寫在一個統(tǒng)一的dispose()方法里模塊卸載時必調(diào)。上線前做一次“打開關閉 20 次”的壓測看進程數(shù)是否回到初始值這是最直接的驗證手段。現(xiàn)象開發(fā)機能跑打成安裝包后安裝到干凈機器上界面一片空白日志里沒有任何異常。原因內(nèi)核二進制沒進安裝包或者進了包但被安全軟件攔在臨時目錄外或者目標機器的用戶目錄沒有寫權限。解決顯式指定chromiumDir到安裝目錄下的子目錄并在安裝包里預置好這份內(nèi)核跳過首次解壓。安裝完成后加一步校驗檢查目錄下chrome可執(zhí)行文件是否存在、動態(tài)庫數(shù)量是否與預期一致。這一步能擋掉大部分“換臺機器就不行”的問題。6. 上線前的自檢把內(nèi)核版本、線程歸屬和關閉行為都打出來調(diào)到后期問題往往不是“跑不起來”而是“跑起來但行為不對”。我習慣在動手寫業(yè)務代碼之前先跑一段自檢把三個不確定因素量化出來內(nèi)核到底是什么版本、回調(diào)落在哪個線程、關閉之后進程有沒有真的退干凈。public final class BrowserSelfCheck { public static void main(String[] args) throws Exception { Engine engine BrowserEngineHolder.get(); System.out.println(engine created on: Thread.currentThread().getName()); Browser browser engine.newBrowser(); browser.navigation().loadUrl(about:blank); browser.navigation().on(FinishLoading.class, event - { // UA 里 Chrome/xx 那段就是內(nèi)核真實版本比看文檔靠譜 System.out.println(userAgent browser.userAgent()); // 回調(diào)線程名能直接告訴你這里能不能碰 Swing System.out.println(callback thread Thread.currentThread().getName()); // 離屏模式下這一行必須 0否則就是白屏 System.out.println(viewport browser.mainFrame().isPresent()); }); Thread.sleep(3000); browser.close(); engine.close(); // 正常情況這行打印后 1~2 秒內(nèi)進程列表里的 Chromium 子進程應全部消失 System.out.println(closed, check process list now); } }三個輸出各有用途。userAgent里的Chrome/后面那段數(shù)字是內(nèi)核真實版本比你翻構(gòu)件元數(shù)據(jù)可靠——如果它和你預期的代際差得遠說明內(nèi)核目錄里躺著一份舊的解壓結(jié)果刪掉chromiumDir重來。callback thread決定了你的回調(diào)體能不能直接操作界面名字不是AWT-EventQueue-0的一律得包invokeLater。最后那句closed打印完之后去任務管理器看一眼子進程如果還在就是某個Browser沒關。這段代碼我一般會做成一個隱藏的“診斷”菜單項讓現(xiàn)場實施的人也能點一下把三行輸出貼回來。比讓用戶描述“有時候會卡”有用得多。從那以后我每次把 jxbrowser 塞進新項目都強制先跑一遍這段自檢再談業(yè)務邏輯——省下來的排查時間比寫這段代碼的時間多得多。手上這份 7.19 的包建議也先按這個順序跑通再加功能。希望幫到你。本文還有配套的精品資源點擊獲取