)
簡(jiǎn)介面向仍在使用 VS2010 與 .NET Framework 4.0 的 WinForm 開(kāi)發(fā)者這份示例工程演示如何借助 CefSharp.WinForms 49.0.1 把 Chromium 內(nèi)核嵌入桌面程序替代老舊的 WebBrowser 控件解決網(wǎng)頁(yè)兼容性與腳本交互能力不足的問(wèn)題屬于可直接上手的中初級(jí)實(shí)戰(zhàn)參考。壓縮包共 319 個(gè)文件、約 189.28MB其中 174 個(gè) pak 與 52 個(gè) dll 構(gòu)成 CEF 運(yùn)行時(shí)依賴與本地化資源另有 7 個(gè) cs 源文件、2 個(gè) csproj 工程及 xml、config、bin、exe 等配套項(xiàng)。代碼覆蓋 NuGet 引用配置、Cef.Initialize 初始化、ChromiumWebBrowser 控件創(chuàng)建、加載完成與狀態(tài)變更事件監(jiān)聽(tīng)、ExecuteScriptAsync 執(zhí)行腳本、CefSettings 定制如禁用 GPU 加速、設(shè)置緩存路徑以及退出時(shí)調(diào)用 Cef.Shutdown 釋放資源。工程可直接編譯運(yùn)行也可作為二次開(kāi)發(fā)骨架已有 3432 人學(xué)習(xí)下載適合需要快速驗(yàn)證內(nèi)嵌瀏覽器方案的 .NET 桌面開(kāi)發(fā)者參考。1. 從一次白屏說(shuō)起VS2010 .NET 4.0 下還能用的 CefSharp.WinForms 示例這份 C# WinForm CefSharp 示例代碼解決的是 VS2010 .NET 4.0 老項(xiàng)目里嵌一個(gè) Chromium 瀏覽器控件的剛需。前陣子維護(hù)一個(gè)模擬項(xiàng)目X客戶端只能跑在 .NET 4.0 上業(yè)務(wù)頁(yè)面已經(jīng)是現(xiàn)代前端構(gòu)建出來(lái)的WebBrowser 控件一打開(kāi)就腳本報(bào)錯(cuò)換成 CefSharp 又連續(xù)白屏。后來(lái)把版本對(duì)齊、初始化順序、輸出目錄和 x86/x64 這幾件事拆開(kāi)驗(yàn)證才整理出一套能編譯、能加載本地 HTML、能做 C# 與 JS 互調(diào)的最小骨架。它適合還在維護(hù)老 WinForm 客戶端、不能隨便升級(jí)框架的人也適合想先跑通 CefSharp 再疊業(yè)務(wù)邏輯的開(kāi)發(fā)者。下面按“選包、起工程、調(diào)生命周期、部署、排錯(cuò)”的順序拆開(kāi)講代碼按 VS2010 的 C# 4.0 語(yǔ)法寫(xiě)不依賴 async/await。2. 版本對(duì)齊先確認(rèn) CefSharp 包里有 net40再談 WinForm 嵌瀏覽器2.1 為什么 VS2010 下不能隨便拿最新 CefSharp 包VS2010 自帶的是 .NET 4.0 和 C# 4.0這兩個(gè)約束會(huì)同時(shí)卡住 CefSharp 的托管層和示例代碼。CefSharp 的托管程序集有目標(biāo)框架要求較新的包往往把 lib 目錄切到 net45、net452 甚至更高直接引用會(huì)提示程序集依賴不兼容C# 4.0 又不支持 async/await網(wǎng)上大量 CefSharp 示例是 C# 5 以后的寫(xiě)法復(fù)制進(jìn) VS2010 會(huì)直接編譯不過(guò)。另一個(gè)容易被忽略的是原生層CefSharp 依賴 libcef.dll、CefSharp.BrowserSubprocess.exe 和一整套 CEF 資源文件它們分 x86 和 x64項(xiàng)目平臺(tái)目標(biāo)設(shè)成 AnyCPU 時(shí)很容易把錯(cuò)誤架構(gòu)的原生 DLL 帶進(jìn)進(jìn)程。我一般選包時(shí)先做三件事解壓 nupkg看 lib 目錄下有沒(méi)有 net40 或 net4.0 文件夾看 nuspec 里對(duì) .NET Framework 的依賴再看 cef.redist.x86 / cef.redist.x64 包里是不是同時(shí)帶了托管 DLL 和原生文件。沒(méi)有 net40 目錄的包不要硬上換一個(gè)能對(duì)上的舊分支比后期改程序集綁定省事。版本號(hào)不必記直接看包內(nèi)目錄和 nuspec 最穩(wěn)。選好之后把 nupkg 當(dāng)壓縮包解壓手工引用 DLL比在 VS2010 里折騰老版本 NuGet 更可控。2.2 手工還原一個(gè)可編譯的工程骨架在 VS2010 里新建 Windows Forms Application目標(biāo)框架選 .NET Framework 4.0。進(jìn)配置管理器把平臺(tái)從 AnyCPU 改成 x86新建平臺(tái)時(shí)不要勾“創(chuàng)建解決方案的平臺(tái)”直接把當(dāng)前項(xiàng)目改成 x86。然后把 CefSharp 包里的 CefSharp.dll、CefSharp.Core.dll、CefSharp.WinForms.dll 加到引用引用屬性里的 Copy Local 設(shè)為 True。原生文件不通過(guò)引用走直接復(fù)制到項(xiàng)目下的 CefSharp 目錄再在項(xiàng)目里包含這些文件屬性設(shè)為“如果較新則復(fù)制”。!-- 項(xiàng)目文件里重點(diǎn)看這幾項(xiàng)等價(jià)于在 VS2010 屬性面板里設(shè)置 -- PropertyGroup TargetFrameworkVersionv4.0/TargetFrameworkVersion PlatformTargetx86/PlatformTarget Prefer32Bitfalse/Prefer32Bit /PropertyGroup這段配置的核心是 TargetFrameworkVersion 和 PlatformTarget。VS2010 的 WinForm 項(xiàng)目模板默認(rèn)可能是 AnyCPUCefSharp 在 64 位系統(tǒng)上會(huì)按進(jìn)程位數(shù)去找原生 DLL如果輸出目錄里只有 x86 的 libcef.dll就會(huì)出現(xiàn)進(jìn)程啟動(dòng)后白屏或直接退出。Prefer32Bit 是給后續(xù)高版本 MSBuild 看的VS2010 下主要靠 PlatformTarget 控制。平臺(tái)目標(biāo)定下來(lái)之后輸出目錄只放對(duì)應(yīng)架構(gòu)的 CEF 文件不要 x86 和 x64 混放混放時(shí)誰(shuí)先被加載看運(yùn)氣玄學(xué)問(wèn)題會(huì)很多。2.3 最小驗(yàn)證先加載 about:blank再加載本地 HTML工程骨架起來(lái)后不要急著接業(yè)務(wù)頁(yè)面。先寫(xiě)一個(gè)最小窗體在 Load 事件里動(dòng)態(tài)創(chuàng)建 ChromiumWebBrowser加載 about:blank確認(rèn)能出空白頁(yè)而不是白屏。這個(gè)步驟能過(guò)濾掉大部分原生 DLL 和子進(jìn)程路徑問(wèn)題。確認(rèn) about:blank 正常后再把 URL 換成 Application.StartupPath 下的 index.html。本地文件路徑用 file:/// 前綴反斜杠換成斜杠中文目錄先避開(kāi)。private void MainForm_Load(object sender, EventArgs e) { // 先加載 about:blank確認(rèn) CefSharp 原生層和子進(jìn)程都正常 var browser new ChromiumWebBrowser(about:blank); browser.Dock DockStyle.Fill; this.Controls.Add(browser); // 原生層確認(rèn)后再換成本地 HTML string htmlPath Application.StartupPath.Replace(\\, /) /index.html; browser.Load(file:/// htmlPath); }這段代碼沒(méi)有用設(shè)計(jì)器拖控件原因是 ChromiumWebBrowser 在設(shè)計(jì)時(shí)初始化會(huì)拉子進(jìn)程VS2010 設(shè)計(jì)器經(jīng)常直接崩。動(dòng)態(tài)創(chuàng)建放在 Form.Load 里設(shè)計(jì)器只看到普通窗體打開(kāi)設(shè)計(jì)器不會(huì)觸發(fā) Cef 初始化。Dock 設(shè)為 Fill 讓瀏覽器鋪滿窗體Load 方法傳 URL 即可。如果 about:blank 都白屏先別改代碼去輸出目錄檢查 libcef.dll、CefSharp.BrowserSubprocess.exe 和 locales 是否齊全再看項(xiàng)目平臺(tái)目標(biāo)和 DLL 架構(gòu)是否一致。3. 把瀏覽器控件用起來(lái)初始化順序、生命周期和 C# 與 JS 互調(diào)3.1 Cef.Initialize 和 Cef.Shutdown 的配對(duì)Cef.Initialize 建議放在 Program.Main 里、Application.Run 之前并且全進(jìn)程只調(diào)用一次。常見(jiàn)錯(cuò)誤是在窗體構(gòu)造函數(shù)里初始化每開(kāi)一個(gè)窗體調(diào)一次第二次就拋異常或者初始化放在 Load 里子進(jìn)程路徑還沒(méi)準(zhǔn)備好。初始化參數(shù)里 BrowserSubprocessPath 必須指向輸出目錄下的 CefSharp.BrowserSubprocess.exeCachePath 和 UserDataPath 指向有寫(xiě)權(quán)限的目錄。老項(xiàng)目裝在 Program Files 下時(shí)Application.StartupPath 往往不可寫(xiě)緩存會(huì)靜默失敗表現(xiàn)就是頁(yè)面加載一半或登錄態(tài)丟失。[STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); // 舊分支的 JS 綁定開(kāi)關(guān)必須在 Cef.Initialize 之前設(shè)置 CefSharpSettings.LegacyJavascriptBindingEnabled true; var settings new CefSettings(); settings.BrowserSubprocessPath Path.Combine(Application.StartupPath, CefSharp.BrowserSubprocess.exe); settings.CachePath Path.Combine(Application.StartupPath, Cache); settings.UserDataPath Path.Combine(Application.StartupPath, UserData); settings.Locale zh-CN; settings.LogSeverity LogSeverity.Info; settings.LogFile Path.Combine(Application.StartupPath, cef.log); try { Cef.Initialize(settings); } catch (Exception ex) { MessageBox.Show(Cef 初始化失敗 ex.Message); return; } Application.Run(new MainForm()); }參數(shù)里 BrowserSubprocessPath 是最容易漏的不設(shè)時(shí) CefSharp 會(huì)按默認(rèn)規(guī)則找子進(jìn)程部署目錄一變就找不到。CachePath 和 UserDataPath 分開(kāi)緩存和用戶數(shù)據(jù)不要塞進(jìn)只讀目錄。Locale 設(shè)成 zh-CN 對(duì)中文界面和輸入法更穩(wěn)。LogSeverity 和 LogFile 是排錯(cuò)用的白屏?xí)r先看 cef.log 里有沒(méi)有“failed to load libcef”或子進(jìn)程啟動(dòng)失敗。CefSharpSettings.LegacyJavascriptBindingEnabled 只對(duì)舊版 RegisterJsObject 生效新版綁定 API 不需要但老分支不設(shè)置會(huì)報(bào)對(duì)象未定義。3.2 ChromiumWebBrowser 的創(chuàng)建時(shí)機(jī)與釋放ChromiumWebBrowser 的創(chuàng)建放在窗體 Load 事件里比較穩(wěn)FormClosing 里先 Dispose 瀏覽器再 Cef.Shutdown。順序反了會(huì)出現(xiàn)關(guān)閉窗體后 CefSharp.BrowserSubprocess.exe 還在任務(wù)管理器里第二次打開(kāi)程序連不上子進(jìn)程。Dispose 之后把引用置空避免 FormClosing 被多次觸發(fā)時(shí)重復(fù)釋放。如果窗體里有多個(gè)瀏覽器實(shí)例每個(gè)實(shí)例都要 Dispose但 Cef.Shutdown 全進(jìn)程只調(diào)一次。private ChromiumWebBrowser browser; private void MainForm_Load(object sender, EventArgs e) { browser new ChromiumWebBrowser(about:blank); browser.Dock DockStyle.Fill; this.Controls.Add(browser); } private void MainForm_FormClosing(object sender, FormClosingEventArgs e) { if (browser ! null) { browser.Dispose(); browser null; } Cef.Shutdown(); }這段代碼的關(guān)鍵是生命周期配對(duì)。Load 里創(chuàng)建FormClosing 里釋放Cef.Shutdown 放在窗體關(guān)閉時(shí)而不是 Application.Exit 事件里避免窗體還沒(méi)釋放就先關(guān) Cef。Dock 設(shè)為 Fill 后不需要再調(diào) Anchor。如果窗體里還有其他控件建議給瀏覽器一個(gè) Panel 容器瀏覽器 Dock 到 Panel業(yè)務(wù)控件放在 Panel 外面后面換皮膚或加工具欄時(shí)不用改瀏覽器布局。3.3 JS 調(diào) C#RegisterJsObject 的綁定時(shí)機(jī)與命名舊分支常用 RegisterJsObject 把 C# 對(duì)象暴露給頁(yè)面。綁定要放在瀏覽器創(chuàng)建之后、頁(yè)面 Load 之前綁定太晚頁(yè)面腳本已經(jīng)執(zhí)行完window.bridge 就是 undefined。第三個(gè)參數(shù) camelCaseJavascriptNames 決定 C# 方法名是否轉(zhuǎn)成首字母小寫(xiě)傳 false 時(shí) JS 里保持 C# 原名傳 true 時(shí) GetVersion 變成 getVersion。這個(gè)參數(shù)和前端約定必須一致否則前端按小寫(xiě)調(diào)、后端按大寫(xiě)綁控制臺(tái)只會(huì)報(bào)未定義。public class JsBridge { public string GetVersion() { return 1.0; } public void Notify(string message) { // JS 調(diào)用可能不在 UI 線程更新控件前用 BeginInvoke } } private void MainForm_Load(object sender, EventArgs e) { browser new ChromiumWebBrowser(about:blank); browser.Dock DockStyle.Fill; this.Controls.Add(browser); // 第三個(gè)參數(shù) falseJS 里用 window.bridge.GetVersion() browser.RegisterJsObject(bridge, new JsBridge(), false); browser.Load(file:/// Application.StartupPath.Replace(\\, /) /index.html); }JsBridge 里的方法盡量簡(jiǎn)單參數(shù)用 string、int、bool 這類基礎(chǔ)類型復(fù)雜對(duì)象讓前端序列化成 JSON 字符串再傳減少綁定層序列化差異。Notify 里如果要更新 WinForm 控件不要直接改JS 調(diào)用線程不一定是 UI 線程用 BeginInvoke 包一層。注冊(cè)對(duì)象名不要用 window、document 這類保留字也不要和頁(yè)面已有全局變量重名重名時(shí)頁(yè)面自己的對(duì)象會(huì)被覆蓋排查起來(lái)很費(fèi)時(shí)間。3.4 C# 調(diào) JS不用 async/await 的 EvaluateScriptAsync 寫(xiě)法VS2010 的 C# 4.0 不支持 async/await但 EvaluateScriptAsync 返回的是 Task可以用 ContinueWith 接。回調(diào)里判斷 task.IsFaulted、task.IsCanceled再取 task.Result.Success 和 task.Result.Result。ContinueWith 的第二個(gè)參數(shù)傳 TaskScheduler.FromCurrentSynchronizationContext()讓回調(diào)回到 UI 線程避免在回調(diào)里直接操作控件時(shí)拋跨線程異常。這個(gè)寫(xiě)法比 Thread.Sleep 等頁(yè)面加載可靠得多。private void ReadDocumentTitle() { browser.EvaluateScriptAsync(document.title).ContinueWith(task { if (task.IsFaulted || task.IsCanceled) { return; } var response task.Result; if (response.Success) { MessageBox.Show(Convert.ToString(response.Result)); } }, TaskScheduler.FromCurrentSynchronizationContext()); }EvaluateScriptAsync 的參數(shù)是 JavaScript 表達(dá)式返回值會(huì)走 JSON 序列化字符串、數(shù)字、布爾值都能直接轉(zhuǎn)DOM 對(duì)象不要直接返回先轉(zhuǎn)成 JSON.stringify 再傳回來(lái)。ContinueWith 里不要假設(shè) task.Result 一定不為空腳本拋異常時(shí) Success 為 falseResult 可能是 null。需要連續(xù)調(diào)多個(gè)腳本時(shí)不要嵌套 ContinueWith容易把線程調(diào)度繞暈拆成多個(gè)方法按業(yè)務(wù)順序調(diào)或者在前端用一個(gè)初始化完成標(biāo)記C# 側(cè)只探這一個(gè)標(biāo)記。4. 部署不是復(fù)制一個(gè) exe輸出目錄、x86/x64 與 VC 運(yùn)行時(shí)的核對(duì)表4.1 輸出目錄該放哪些文件CefSharp 的部署目錄不是只有一個(gè) exe托管 DLL、原生 DLL、子進(jìn)程、資源文件、語(yǔ)言包都要在。不同分支文件名單會(huì)變最穩(wěn)的辦法是看 cef.redist 包里的 README 或示例輸出目錄把那一套整體復(fù)制過(guò)來(lái)再刪掉不需要的語(yǔ)言包。下面這張表是常見(jiàn)必須項(xiàng)用來(lái)核對(duì)有沒(méi)有漏不是固定清單。文件或目錄作用常見(jiàn)漏法CefSharp.dll托管公共接口只引用了 WinForms 包漏了公共接口CefSharp.Core.dll托管核心引用有輸出目錄沒(méi)有CefSharp.WinForms.dllWinForms 控件項(xiàng)目引用正常發(fā)布時(shí)漏復(fù)制CefSharp.BrowserSubprocess.exe瀏覽器子進(jìn)程沒(méi)設(shè) BrowserSubprocessPath或沒(méi)復(fù)制libcef.dllCEF 原生核心架構(gòu)不對(duì)或被殺軟隔離chrome_elf.dll加載與崩潰輔助只復(fù)制 libcef.dll漏了它icudtl.datICU 數(shù)據(jù)頁(yè)面中文亂碼或直接白屏natives_blob.bin、snapshot_blob.binV8 快照啟動(dòng)時(shí)報(bào)快照加載失敗locales 目錄語(yǔ)言包只放 en-US.pak中文環(huán)境異常swiftshader 或 d3dcompiler_47.dll軟件渲染與圖形依賴虛擬機(jī)或無(wú) GPU 環(huán)境黑屏核對(duì)時(shí)不要只看項(xiàng)目引用列表要看 bin 目錄實(shí)際輸出。VS2010 里非引用文件靠“復(fù)制到輸出目錄”屬性控制有時(shí)候文件在項(xiàng)目里屬性還是“不復(fù)制”調(diào)試時(shí)從 obj 目錄能跑發(fā)布就缺文件。建議在項(xiàng)目里建一個(gè) CefSharp 文件夾把原生文件全部放進(jìn)去屬性統(tǒng)一設(shè)成“如果較新則復(fù)制”發(fā)布時(shí)整目錄帶走。4.2 平臺(tái)目標(biāo)與原生 DLL 架構(gòu)AnyCPU 為什么容易翻車(chē)AnyCPU 在 64 位系統(tǒng)上默認(rèn)跑 64 位進(jìn)程但很多 CefSharp 包默認(rèn)帶的是 x86 原生文件進(jìn)程位數(shù)和 DLL 架構(gòu)對(duì)不上libcef.dll 加載失敗表現(xiàn)就是窗體出來(lái)但瀏覽器區(qū)域全白。反過(guò)來(lái)項(xiàng)目設(shè)了 x64輸出目錄卻是 x86 的 cef.redist也一樣白屏。排查時(shí)先看任務(wù)管理器里進(jìn)程有沒(méi)有“32 位”標(biāo)記再用 dumpbin 看 libcef.dll 的 machine 字段兩邊必須一致。:: 在 Visual Studio 命令提示符里執(zhí)行查看 libcef.dll 是 x86 還是 x64 dumpbin /headers libcef.dll | findstr machine輸出里 14C 表示 x868664 表示 x64。項(xiàng)目平臺(tái)目標(biāo)設(shè) x86 時(shí)輸出目錄只放 x86 的 CEF 文件設(shè) x64 時(shí)只放 x64。不要為了兼容兩種機(jī)器在一個(gè)目錄里混放CefSharp 不會(huì)自動(dòng)按位數(shù)切目錄混放時(shí)加載到哪個(gè)看文件系統(tǒng)順序。確實(shí)要同時(shí)支持 32 位和 64 位就出兩個(gè)安裝包或者啟動(dòng)器里判斷 IntPtr.Size 后切工作目錄普通業(yè)務(wù)客戶端沒(méi)必要做這么復(fù)雜。4.3 VC 運(yùn)行時(shí)與日志白屏?xí)r先看 cef.logCEF 原生層依賴 VC 運(yùn)行時(shí)缺運(yùn)行庫(kù)時(shí)進(jìn)程可能直接退出也可能彈一個(gè)系統(tǒng)錯(cuò)誤框后白屏。不要憑記憶裝年份用 Dependencies 或 dumpbin /dependents 看 libcef.dll 依賴哪些 msvcp、vcruntime缺哪個(gè)裝哪個(gè)x86 和 x64 分開(kāi)裝。裝完重啟一下客戶端再做驗(yàn)證有時(shí)候運(yùn)行庫(kù)剛裝完當(dāng)前進(jìn)程還沒(méi)重新加載。var settings new CefSettings(); settings.LogSeverity LogSeverity.Info; settings.LogFile Path.Combine(Application.StartupPath, cef.log);日志文件要放在有寫(xiě)權(quán)限的目錄Program Files 下普通用戶不可寫(xiě)時(shí)日志不會(huì)生成看起來(lái)像“沒(méi)日志”實(shí)際是寫(xiě)不進(jìn)去。cef.log 里重點(diǎn)看 libcef.dll 加載路徑、BrowserSubprocess 啟動(dòng)參數(shù)、GPU 初始化失敗、字體加載失敗。GPU 相關(guān)報(bào)錯(cuò)可以先加settings.DisableGpu true驗(yàn)證是不是顯卡驅(qū)動(dòng)或虛擬機(jī)渲染問(wèn)題確認(rèn)后再?zèng)Q定是否長(zhǎng)期禁用。日志級(jí)別不要長(zhǎng)期開(kāi) Verbose文件漲得很快排完障改回 Info 或 Disable。5. 避坑與排查白屏、設(shè)計(jì)器閃退、JS 未定義、進(jìn)程殘留的 5 條記錄5.1 設(shè)計(jì)器一打開(kāi)就崩別把 ChromiumWebBrowser 拖進(jìn)設(shè)計(jì)器現(xiàn)象是雙擊 MainForm.cs 進(jìn)設(shè)計(jì)器VS2010 停幾秒后直接退出或者彈“未將對(duì)象引用設(shè)置到對(duì)象的實(shí)例”。原因是 ChromiumWebBrowser 在設(shè)計(jì)時(shí)也會(huì)走構(gòu)造函數(shù)觸發(fā) Cef 初始化和子進(jìn)程創(chuàng)建設(shè)計(jì)器進(jìn)程里沒(méi)有完整的 CEF 運(yùn)行環(huán)境直接崩。解決方式是窗體上不放瀏覽器控件在 Load 事件里用代碼創(chuàng)建設(shè)計(jì)器里只留一個(gè) Panel 占位。已經(jīng)拖進(jìn)去的把控件從設(shè)計(jì)器刪掉構(gòu)造函數(shù)里的初始化代碼移到 Load 里重新生成后再開(kāi)設(shè)計(jì)器。5.2 運(yùn)行后白屏或黑屏先查 libcef.dll 和架構(gòu)現(xiàn)象是窗體正常出來(lái)瀏覽器區(qū)域全白cef.log 里可能有“failed to load libcef.dll”或“browser subprocess exited”。原因按概率排輸出目錄沒(méi)有 libcef.dlllibcef.dll 架構(gòu)和進(jìn)程位數(shù)不一致BrowserSubprocessPath 沒(méi)設(shè)置或路徑不對(duì)VC 運(yùn)行時(shí)缺失。解決順序是先看輸出目錄文件齊不齊再用 dumpbin 看架構(gòu)然后確認(rèn) BrowserSubprocessPath 指向?qū)嶋H exe最后用 Dependencies 查 libcef.dll 的依賴。四項(xiàng)都過(guò)還白屏把 LogSeverity 開(kāi)到 Verbose看日志里第一個(gè) error 出現(xiàn)在哪一步不要從最后一個(gè)報(bào)錯(cuò)往前猜。5.3 編譯報(bào) async/await 錯(cuò)誤VS2010 的 C# 4.0 不認(rèn)現(xiàn)象是從網(wǎng)上復(fù)制的 CefSharp 示例粘貼進(jìn)項(xiàng)目后async、await關(guān)鍵字下面全是紅線錯(cuò)誤提示“找不到類型或命名空間名稱 async”之類。原因是 VS2010 默認(rèn) C# 4.0async/await 是 C# 5 才有的語(yǔ)法。解決方式不是硬升框架而是把異步調(diào)用改成 ContinueWith 寫(xiě)法EvaluateScriptAsync 返回 Task用 TaskScheduler.FromCurrentSynchronizationContext() 回 UI 線程。如果項(xiàng)目里大量 async 代碼考慮單獨(dú)抽一個(gè) C# 5 的輔助程序集但老項(xiàng)目里通常不值得按最小改動(dòng)改成回調(diào)更穩(wěn)。5.4 JS 調(diào)用 C# 報(bào) undefined綁定時(shí)機(jī)和命名踩坑現(xiàn)象是頁(yè)面控制臺(tái)報(bào)window.bridge is undefined或者bridge.GetVersion is not a function。原因常見(jiàn)三種RegisterJsObject 在頁(yè)面 Load 之后才調(diào)用camelCaseJavascriptNames 參數(shù)和前端調(diào)用大小寫(xiě)不一致LegacyJavascriptBindingEnabled 沒(méi)在 Cef.Initialize 之前打開(kāi)。解決方式是先確認(rèn)綁定代碼在 Load 之前執(zhí)行再在頁(yè)面里打印Object.keys(window.bridge)看實(shí)際暴露了哪些方法最后核對(duì)注冊(cè)時(shí)第三個(gè)參數(shù)。舊分支的 RegisterJsObject 對(duì)綁定對(duì)象名敏感名字里不要帶點(diǎn)、橫線前端調(diào)用時(shí)對(duì)象名和方法名分開(kāi)檢查。5.5 關(guān)閉窗體后進(jìn)程殘留Cef.Shutdown 與 Dispose 的順序現(xiàn)象是主窗體關(guān)了任務(wù)管理器里 CefSharp.BrowserSubprocess.exe 還在再次啟動(dòng)程序時(shí)頁(yè)面加載失敗或直接白屏。原因是 ChromiumWebBrowser 沒(méi) Dispose或者 Cef.Shutdown 調(diào)用太早/太晚子進(jìn)程沒(méi)收到退出信號(hào)。解決方式是在 FormClosing 里先 browser.Dispose()把引用置空再 Cef.Shutdown()。如果窗體是多文檔結(jié)構(gòu)每個(gè)瀏覽器實(shí)例都要 DisposeCef.Shutdown 只調(diào)一次。調(diào)試時(shí)可以在 Shutdown 前后打日志確認(rèn)順序不要用 Application.Exit 直接殺進(jìn)程。6. 進(jìn)階技巧用 BrowserInitializedChanged 和請(qǐng)求頭把老控件跑穩(wěn)老項(xiàng)目里最容易寫(xiě)壞的是“等頁(yè)面加載”這段邏輯。很多人用 Thread.Sleep(3000) 然后 EvaluateScriptAsync機(jī)器慢的時(shí)候頁(yè)面沒(méi)好機(jī)器快的時(shí)候白等三秒。我一般用 BrowserInitializedChanged 事件確認(rèn)瀏覽器底層初始化完成再用 FrameLoadEnd 判斷主框架加載結(jié)束最后用 EvaluateScriptAsync 探 document.readyState。三層判斷比固定睡眠穩(wěn)也不會(huì)把 UI 線程卡住。下面這段只保留關(guān)鍵分支事件簽名以你包內(nèi) API 為準(zhǔn)。browser.BrowserInitializedChanged (sender, args) { if (!browser.IsBrowserInitialized) { return; } // 初始化完成后再加載業(yè)務(wù)頁(yè)面避免在 about:blank 階段搶跑 browser.Load(file:/// Application.StartupPath.Replace(\\, /) /index.html); }; browser.FrameLoadEnd (sender, args) { if (!args.Frame.IsMain) { return; } // 主框架加載完再探 readyState比固定 Sleep 可靠 browser.EvaluateScriptAsync(document.readyState).ContinueWith(task { if (task.IsFaulted || task.IsCanceled || !task.Result.Success) { return; } // 這里的返回值可以記日志也可以觸發(fā)后續(xù)業(yè)務(wù)初始化 }, TaskScheduler.FromCurrentSynchronizationContext()); };另一個(gè)實(shí)用技巧是給所有請(qǐng)求加自定義頭。老客戶端經(jīng)常需要后端識(shí)別“這是 WinForm 內(nèi)嵌瀏覽器”但又不想改頁(yè)面代碼。在自定義 IRequestHandler 的 OnBeforeResourceLoad 里給 request.Headers 塞一個(gè)固定鍵值返回 false 表示繼續(xù)請(qǐng)求。這樣后端按頭分流前端頁(yè)面不用動(dòng)。注意舊分支 IRequestHandler 接口成員較多除了關(guān)鍵方法其余成員按接口定義返回默認(rèn)值不要只實(shí)現(xiàn)一個(gè)方法就編譯。public class CustomRequestHandler : IRequestHandler { public bool OnBeforeResourceLoad(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback) { // 給每個(gè)請(qǐng)求打標(biāo)后端據(jù)此識(shí)別客戶端類型 request.Headers[X-Client] WinFormCef; return false; } // 其余接口成員按包內(nèi)定義補(bǔ)全默認(rèn)返回 false 或繼續(xù) }緩存目錄也值得單獨(dú)說(shuō)一下。默認(rèn)把 CachePath 放在 Application.StartupPath 下調(diào)試時(shí)沒(méi)問(wèn)題裝到 Program Files 后普通用戶沒(méi)有寫(xiě)權(quán)限緩存寫(xiě)入失敗不一定報(bào)錯(cuò)表現(xiàn)是頁(yè)面加載慢、登錄態(tài)丟、靜態(tài)資源重復(fù)下載。我一般把 CachePath 和 UserDataPath 改到 LocalApplicationData 下按產(chǎn)品名建子目錄啟動(dòng)時(shí)先 Directory.CreateDirectory再傳給 CefSettings。這樣升級(jí)安裝包不會(huì)把緩存清掉也不會(huì)因?yàn)闄?quán)限問(wèn)題出現(xiàn)“有時(shí)好有時(shí)壞”的玄學(xué)。驗(yàn)證時(shí)加載一個(gè)帶版本號(hào)的靜態(tài)頁(yè)刷新兩次看 Network 里有沒(méi)有 304或者看緩存目錄有沒(méi)有實(shí)際文件生成。從那以后我每次接老 WinForm 嵌瀏覽器的活都先做一個(gè)只有 about:blank 的最小 Demo確認(rèn) libcef.dll 架構(gòu)、BrowserSubprocessPath、VC 運(yùn)行時(shí)和 Cef.Shutdown 這四件事都過(guò)再往里面疊業(yè)務(wù)頁(yè)面和 JS 互調(diào)。最小 Demo 跑不通時(shí)不要改業(yè)務(wù)代碼先看 cef.log 和輸出目錄能省掉大量來(lái)回猜的時(shí)間。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取