ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

在 .NET 4.0 与 VS2010 的 WinForm 中集成 CefSharp Chromium

在 .NET 4.0 与 VS2010 的 WinForm 中集成 CefSharp Chromium

简介:面向使用VS2010与.NET 4.0的C# WinForm开发者,CefSharp.WinForms 49.0.1示例包提供内嵌Chromium浏览器工程,适合需要补充网页浏览、脚本交互与加载监控能力的初、中级开发者。压缩包共319个文件、约189.28MB,以pak资源、dll动态库、xml与config配置为主,并含cs源码、csproj项目、sln解决方案及nupkg依赖,兼顾运行与二次开发。已有3433人学习下载。示例围绕Cef初始化、Chromium控件、导航与加载事件、执行JavaScript、CefSettings配置及Cef.Shutdown释放展开,保留工程结构,便于排查引用缺失、运行库放置和资源加载问题,搭建WinForm浏览器原型。

1. 一个不能升级 .NET 4.0 的老项目,怎么把 Chromium 塞进 WinForm

场景很具体:一套跑了好几年的 C# WinForm 系统,目标框架锁死在 .NET 4.0,开发机上是 VS2010,业务方要求界面里嵌一个现代浏览器,因为原来那个 WebBrowser 控件用的是老 IE 内核,前端随便用点新语法就白屏。这种条件下,在 WinForm 里内嵌 Chromium 最直接的方案就是 CefSharp,控件拖上去、Dock 设成 Fill,用法和普通 WinForm 控件差别不大。真正麻烦的是版本:VS2010 的编译器只到 C# 4.0,运行时只到 .NET 4.0,能选的 CefSharp 版本被夹在一个很窄的区间里。装错版本、平台目标留成 AnyCPU、输出目录少一个 dll,最后的表现都是同一副面孔——黑屏、白屏,或者启动直接抛异常。

提示:后面所有代码都按 C# 4.0 语法写,没有async/await,没有字符串插值,没有?.。你在网上抄到的示例大概率是 C# 5 以上的写法,粘进 VS2010 直接编译不过。

2. 版本对表:CefSharp 与 .NET 4.0、VS2010 的可用组合

2.1 判断依据是 lib 目录,不是博客里的版本号

CefSharp 的版本号跟着 Chromium 主版本走,43.x、45.x、49.x这些数字背后其实是三件不同的事:Chromium 内核版本、CefSharp 托管层 API、以及最低 .NET 框架要求。这三者里只有最后一个决定你能不能用。.NET 4.0 这条线大致落在 43 及更早的分支上,45 之后最低目标基本抬到了 .NET 4.5,再往后还有 4.5.2 的门槛。

不要记版本号,记方法。把 NuGet 包拉下来之后,看包里的目录结构:

packages\CefSharp.Common.43.0.1\lib\ net40\ <- 有这个目录,.NET 4.0 才有戏 CefSharp.dll CefSharp.Core.dll net45\ <- 只有这个,说明你得升框架

lib下没有net40,就换下一个更早的主版本继续试。这个方法比任何一篇博客都可靠,因为包的目录结构是编译器认的硬事实。我一般会在动手写代码之前,先把这一步确认掉,否则后面调半天初始化,回头发现是框架版本不对,纯属浪费时间。

还有一个容易被忽略的点:VS2010 新建 WinForm 项目时,默认的目标框架经常是.NET Framework 4 Client Profile,不是完整的.NET Framework 4。Client Profile 裁掉了不少程序集,CefSharp 依赖的东西不全,编译期可能就报缺引用,或者运行期抛一些莫名其妙的类型加载失败。项目属性里把目标框架明确改成完整版,这一步别省。

2.2 用 nuget.exe 在 VS2010 里拉包

VS2010 自带的扩展管理器对 NuGet 的支持很有限,插件版本一高就装不上。稳妥的办法是绕开 IDE,直接用命令行把包装进packages目录,再手动往 csproj 里加引用。

:: 在解决方案同级目录执行,输出到 packages 文件夹 nuget.exe install CefSharp.WinForms -Version 43.0.1 -OutputDirectory packages nuget.exe install CefSharp.Common -Version 43.0.1 -OutputDirectory packages

-Version里的数字换成你在上一步确认过、确实带net40目录的那个 patch 版本;-OutputDirectory建议就用packages,和 VS 自带的包管理习惯保持一致,后面写 HintPath 的时候路径好推算。CefSharp.WinForms会带一个对CefSharp.Common的依赖,两个都显式写出来,是为了让版本严格对齐——一个 43.0.1 配一个 43.0.0,运行时会出现程序集版本冲突。

装完之后建一个packages.config放在项目目录下,内容按包的实际版本填:

<?xml version="1.0" encoding="utf-8"?> <packages> <package id="CefSharp.Common" version="43.0.1" targetFramework="net40" /> <package id="CefSharp.WinForms" version="43.0.1" targetFramework="net40" /> </packages>

然后手工编辑 csproj,把三个托管程序集引进来:

<ItemGroup> <Reference Include="CefSharp"> <HintPath>..\packages\CefSharp.Common.43.0.1\lib\net40\CefSharp.dll</HintPath> </Reference> <Reference Include="CefSharp.Core"> <HintPath>..\packages\CefSharp.Common.43.0.1\lib\net40\CefSharp.Core.dll</HintPath> </Reference> <Reference Include="CefSharp.WinForms"> <HintPath>..\packages\CefSharp.WinForms.43.0.1\lib\net40\CefSharp.WinForms.dll</HintPath> </Reference> </ItemGroup>

HintPath是相对 csproj 文件的路径,多一层少一层目录都会导致引用变成黄色感叹号。CefSharp.Core.dll是个混合程序集(托管 + 本地),它运行时还要去加载libcef.dll,所以它不能只躺在 packages 目录里,必须随生成一起进输出目录——这件事由包的 targets 文件负责,前提是你的项目属性配置正确。

2.3 平台目标:AnyCPU 是这份示例代码最容易翻车的地方

VS2010 新建项目的默认配置是AnyCPU,32 位系统上跑 32 位进程,64 位系统上跑 64 位进程。而 CefSharp 的 natives 是按平台分开打包的,一个进程里只能加载一种位数的libcef.dll。两边对不上,启动时就是那句经典的:

未能加载文件或程序集“CefSharp.Core.dll”或它的某一个依赖项。 试图加载格式不正确的程序。 (异常来自 HRESULT:0x8007000B)

解决办法是把平台目标钉死。在项目属性 → 生成 → 目标平台里选x86,或者直接改 csproj:

<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' "> <DebugSymbols>true</DebugSymbols> <OutputPath>bin\x86\Debug\</OutputPath> <PlatformTarget>x86</PlatformTarget> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' "> <OutputPath>bin\x86\Release\</OutputPath> <PlatformTarget>x86</PlatformTarget> <Optimize>true</Optimize> </PropertyGroup>

PlatformTarget决定编译器生成 32 位还是 64 位的宿主进程,OutputPath顺手分开,避免 x86 和 x64 的 natives 混在同一个bin\Debug里互相覆盖。如果你的目标机全是 64 位系统,也可以反过来选x64,但要注意:解决方案配置管理器里那个平台下拉框必须和项目属性里的PlatformTarget一致,我见过只改了其中一个、另一个还是 AnyCPU 的情况,结果是编译出来的 exe 依然是 AnyCPU,异常照报不误。

选 x86 的另一个理由跟老项目有关:很多 VB6 组件、老式 COM 控件、32 位数据库驱动只提供 32 位版本。这种项目改成 x64 会牵连出一堆别的问题,不如整个进程统一在 32 位下跑,CefSharp 也一起用 x86 的 natives,省事。

3. 跑起第一个窗口:Cef.Initialize 与 ChromiumWebBrowser 的装配顺序

3.1 CefSettings 里真正影响成败的几个参数

CefSettings字段很多,但在 .NET 4.0 + WinForm 这个组合里,真正会咬人的就那么几个。

参数建议值作用与不设的后果
MultiThreadedMessageLooptrueCEF 自带消息循环,否则需要自己在 WinForm 的消息泵里调Cef.DoMessageLoopWork(),漏调就卡住
CachePath%LOCALAPPDATA%\应用名\Cache缓存、Cookie、LocalStorage 的落脚点,不设会落到系统临时目录,重装即丢
LogFile%LOCALAPPDATA%\应用名\cef.log出问题时唯一的黑匣子,排查全靠它
LogSeverityInfo(排障期)/Disable(上线后)日志级别,排障时开低了等于没开
AcceptLanguageListzh-CN影响navigator.language和部分站点返回的语言版本
Localezh-CN影响 CEF 自带界面(右键菜单、错误页)的语言
BrowserSubprocessPath一般不设默认会在 exe 同级找CefSharp.BrowserSubprocess.exe,改目录结构时才需要指定

CefCommandLineArgs是个字符串字典,用来给 Chromium 追加命令行开关。在虚拟机、远程桌面、老集成显卡这几类环境里,GPU 合成经常出问题,表现是窗口一片黑但页面其实已经加载完了。加这两个开关是最省事的绕过方式:

settings.CefCommandLineArgs.Add("disable-gpu", "1"); settings.CefCommandLineArgs.Add("disable-gpu-compositing", "1");

代价是页面渲染走软件路径,复杂动画会掉帧。所以我的习惯是先不加,跑起来看是不是黑屏;确认是 GPU 的锅再加回去,而不是一上来就关掉,把性能问题留给以后。

3.2 窗体代码:从 Program.Main 到 Dock=Fill

初始化顺序是硬性的:Cef.Initialize必须在第一个ChromiumWebBrowser实例创建之前调用,而且只能调一次。

using System; using System.IO; using System.Windows.Forms; using CefSharp; namespace CefSharpDemo { internal static class Program { [STAThread] private static void Main() { string root = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "CefSharpDemo"); Directory.CreateDirectory(root); var settings = new CefSettings(); settings.CachePath = Path.Combine(root, "Cache"); settings.LogFile = Path.Combine(root, "cef.log"); settings.LogSeverity = LogSeverity.Info; settings.AcceptLanguageList = "zh-CN"; settings.Locale = "zh-CN"; settings.PersistSessionCookies = true; settings.MultiThreadedMessageLoop = true; // WinForm 下必须显式打开 if (!Cef.Initialize(settings)) { MessageBox.Show("CEF 初始化失败,检查 " + settings.LogFile); return; } Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new MainForm()); Cef.Shutdown(); // 只调一次,放在 Application.Run 之后 } } }

Cef.Initialize返回 false 通常意味着 natives 加载失败或者参数冲突,真正的错误信息在cef.log里。PersistSessionCookies打开之后,登录态能跨进程保留,对一个内嵌业务系统的客户端来说是刚需,不打开的话每次重启都要重新登录。

窗体这边,关键在 Dock 顺序和控件添加顺序。

using System; using System.Windows.Forms; using CefSharp; using CefSharp.WinForms; namespace CefSharpDemo { public partial class MainForm : Form { private ChromiumWebBrowser _browser; private TextBox _address; private Button _go; public MainForm() { InitializeComponent(); Text = "CefSharp Demo"; Width = 1024; Height = 700; BuildLayout(); } private void BuildLayout() { _address = new TextBox { Dock = DockStyle.Top, Height = 24 }; _go = new Button { Dock = DockStyle.Top, Height = 26, Text = "打开" }; _browser = new ChromiumWebBrowser("about:blank"); _browser.Dock = DockStyle.Fill; // 添加顺序决定 Dock 的填充顺序:先加 Fill,再加 Top Controls.Add(_browser); Controls.Add(_go); Controls.Add(_address); _go.Click += delegate { _browser.Load(_address.Text); }; _browser.LoadingStateChanged += Browser_LoadingStateChanged; _browser.ConsoleMessage += Browser_ConsoleMessage; } private void Browser_LoadingStateChanged(object sender, LoadingStateChangedEventArgs e) { if (InvokeRequired) { BeginInvoke(new Action<object, LoadingStateChangedEventArgs>( Browser_LoadingStateChanged), sender, e); return; } _go.Enabled = !e.IsLoading; if (!e.IsLoading) _address.Text = _browser.Address; } private void Browser_ConsoleMessage(object sender, ConsoleMessageEventArgs e) { System.Diagnostics.Debug.WriteLine("[JS] " + e.Message + " @ " + e.Source + ":" + e.Line); } } }

Controls.Add的顺序直接影响布局:WinForm 的 Dock 是后加的先占位,如果先加 Top 的控件再加 Fill 的浏览器,浏览器会把顶部那一块盖掉,表现为地址栏看不见。把浏览器第一个加进去、或者加完之后调SetChildIndex(_browser, 0),两种写法都行。

LoadingStateChanged和ConsoleMessage这两个事件都是从 CEF 的线程抛上来的,不是 UI 线程。第一行InvokeRequired的判断不能省,否则偶发的跨线程访问异常会在你不注意的时候崩掉整个进程。这类异常最难查,因为它跟操作时序有关,开发机上跑一天都不出现,客户那边一点就崩。

3.3 退出时的 Cef.Shutdown 与顺序问题

Cef.Shutdown()调用时机不对,后果是进程退不干净:任务管理器里留着CefSharp.BrowserSubprocess.exe,或者主窗口关了但进程常驻。规则有两条:一是放在Application.Run返回之后,二是整个进程只调一次。

有一个常见的错误写法是在MainForm.FormClosing里调Cef.Shutdown()。这时候浏览器控件还没 Dispose,CEF 的渲染进程还在跑,Shutdown 会被阻塞或者直接抛异常。如果真的需要在窗体关闭时做清理(比如保存 Cookie),正确做法是在窗体的FormClosed里先_browser.Dispose(),把 Shutdown 留给 Main。

还有Environment.Exit式的强杀,这个会绕过 CEF 的所有清理逻辑,下次启动时缓存目录可能是半损坏状态,表现为页面加载异常缓慢或者某些静态资源一直 404。只要用正常的关窗流程,这些问题都不会出现。

4. 双向通信:C# 调 JS 与 JS 调 C# 的两条通道

4.1 C# 4.0 里没有 await,脚本调用怎么写

CefSharp 的脚本执行 API 返回的是Task,网上示例清一色await browser.EvaluateScriptAsync(...)。VS2010 编译不了这个语法,得换成ContinueWith。两种情况分开处理:不关心返回值用ExecuteScriptAsync,需要拿结果用EvaluateScriptAsync。

// 不关心返回值:执行完就完了 private void CallJsNoResult(string method, string arg) { string literal = ToJsString(arg); _browser.ExecuteScriptAsync("window." + method + "(" + literal + ");"); } // 需要返回值:用 ContinueWith 接住结果,不要 .Result 阻塞 UI 线程 private void CallJsWithResult(string expression, Action<string> onDone) { _browser.EvaluateScriptAsync(expression).ContinueWith(t => { if (!t.IsFaulted && t.Result != null && t.Result.Success) onDone(t.Result.Result == null ? string.Empty : t.Result.Result.ToString()); else onDone(string.Empty); }, TaskScheduler.FromCurrentSynchronizationContext()); } // 把 C# 字符串安全地拼进 JS 字面量,这一步千万别省 private static string ToJsString(string s) { if (s == null) return "null"; return "\"" + s.Replace("\\", "\\\\").Replace("\"", "\\\"") .Replace("\r", "\\r").Replace("\n", "\\n") + "\""; }

TaskScheduler.FromCurrentSynchronizationContext()是这段代码里最关键的一个参数。没有它,ContinueWith里的回调跑在线程池线程上,你在回调里更新控件就会触发跨线程异常。加上之后,回调会自动回到 UI 线程的同步上下文,写法和同步代码一样自然。

ToJsString那个函数看着土,但必须写。业务数据里带个引号、反斜杠、换行,直接拼进脚本就是语法错误,而且报错信息只会在ConsoleMessage里冒一行红字,很容易被忽略。血泪经验是:所有拼进ExecuteScriptAsync的外部字符串,一律先过这个函数。

4.2 RegisterJsObject 的注册时机与签名约束

反向通道是 JS 调 C#,靠RegisterJsObject把一个托管对象挂到window上。它有两个硬约束:注册对象的方法签名必须是简单类型进、可序列化类型出;注册动作必须在页面开始加载之前完成。

public class JsBridge { private readonly MainForm _owner; public JsBridge(MainForm owner) { _owner = owner; } // 无参,返回字符串 public string GetVersion() { return "1.0.0"; } // 单参,返回 JSON 字符串由 JS 自己 parse public string QueryOrder(string orderNo) { if (_owner.InvokeRequired) { // 桥方法在 CEF 的线程被调用,必须切回 UI 线程 return (string)_owner.Invoke(new Func<string, string>(QueryOrder), orderNo); } return _owner.BuildOrderJson(orderNo); } // 通知类,用 BeginInvoke 避免 UI 线程等 CEF 造成死锁 public string Notify(string message) { if (_owner.InvokeRequired) { _owner.BeginInvoke(new Action<string>(m => _owner.ShowTip(m)), message); return "{}"; } _owner.ShowTip(message); return "{}"; } }

注册放在浏览器控件构造之后、Load之前:

_browser = new ChromiumWebBrowser("about:blank"); // 必须在 Load 之前注册,页面里的脚本才拿得到 window.bridge _browser.RegisterJsObject("bridge", new JsBridge(this)); _browser.Load("http://127.0.0.1:8080/index.html");

时机错了的表现是 JS 里bridge is not defined,但页面本身一切正常,非常容易被误判成前端问题。我一开始就是把注册写在了FrameLoadEnd事件里,调试了半小时才发现是顺序问题。

返回值我统一用 JSON 字符串,而不是直接返回复杂对象。老版本 CefSharp 对复杂对象的自动序列化有边界,遇到DateTime、泛型集合、循环引用的时候行为不好预测。统一在 C# 端用 JSON 序列化好,JS 端JSON.parse一下,两边都清爽。

JS 侧的调用长这样:

// 同步拿字符串,简单直接 var version = bridge.GetVersion(); // 返回 JSON 的,自己解析 var order = JSON.parse(bridge.QueryOrder("SO20240115001")); console.log(order.status);

4.3 跨线程回 UI:别在 CEF 回调里直接摸控件

除了桥对象,所有 CEF 事件回调都在非 UI 线程。前面LoadingStateChanged里那段InvokeRequired判断,应该是所有事件处理函数的肌肉记忆。判断做漏了,短时间测试不出来,因为跨线程访问控件在 .NET 2.0 之后默认不抛异常,只是行为未定义——可能是界面不刷新,可能是布局错乱,也可能在某个低概率路径上崩掉。

Invoke和BeginInvoke的选择也有讲究:Invoke是同步等待 UI 线程执行完再继续,如果 UI 线程此刻正在等 CEF 的某个结果,双方互等就死锁了。桥对象的查询类方法(要拿返回值给 JS)用Invoke,通知类方法(发个消息给界面显示)用BeginInvoke并直接返回一个空对象。这个区分在功能简单的 Demo 里看不出差别,一旦业务上有大文件加载、模态对话框,差别就是卡死和不卡死。

还有一个坑:桥对象的方法不要做耗时操作。JS 调用是同步阻塞的(在 JS 侧看起来像同步函数),你在里面查个数据库、等个接口,页面直接僵住,用户会以为程序死了。耗时逻辑要么改成事件回调,要么在 C# 端异步跑完之后通过ExecuteScriptAsync回推给页面。

5. 部署排查:从 BadImageFormatException 到白屏的五个现场

5.1 输出目录到底要拷哪些文件

CefSharp 的 NuGet 包带 targets 文件,生成时会自动把 natives 拷到输出目录。前提是项目配置正确(平台目标不是 AnyCPU)。如果你像我一样是手工加引用、没用包管理器的完整流程,那就得知道输出目录里到底该有什么。

文件/目录缺失后的表现
libcef.dll报“找不到指定的模块”,进程直接起不来
CefSharp.Core.dll报找不到程序集,或 BadImageFormatException
CefSharp.BrowserSubprocess.exe主窗口能开,页面一片空白,日志里提示子进程启动失败
cef.pak/cef_100_percent.pak/cef_200_percent.pak页面能渲染但样式错乱,图标全丢
devtools_resources.pak不影响显示,F12 开发者工具打不开
icudtl.dat中文乱码、排序异常
locales\目录(至少含zh-CN.pak)右键菜单英文,部分页面日期格式不对
natives_blob.bin/snapshot_blob.bin启动即崩,日志里是 V8 初始化失败

最后两个 blob 文件名在不同主版本之间会变(新版本合并成了v8_context_snapshot.bin),所以升级版本之后不要只替换 dll,整个输出目录重新生成一遍最保险。

还有一个细节:locales目录里的 pak 文件是按需加载的,settings.Locale设成zh-CN就必须有对应的zh-CN.pak。只拷en-US.pak而 Locale 设成中文,CEF 会回退到默认语言,然后在日志里写一条 warning,不影响主流程但很难看。

5.2 五条踩坑记录:现象、原因、解决

第一条:启动即抛 BadImageFormatException,提示格式不正确的程序。现象是双击 exe 立刻弹异常框,堆栈指向CefSharp.Core.dll的静态构造。原因是进程位数和 natives 位数对不上,多数情况是项目平台目标留成了 AnyCPU,在 64 位系统上跑出了 64 位进程,而拷进去的是 32 位的libcef.dll。解决:项目属性里把平台目标改成x86,解决方案配置管理器的平台也改成x86,清空bin和obj重新生成,最后用任务管理器确认进程没有“(32 位)”以外的标记。

第二条:FileNotFoundException,提示找不到指定的模块。注意是“找不到指定的模块”,不是“找不到文件”。这句话的真实含义是:libcef.dll找到了,但它自己依赖的某个系统 dll 没找到。十有八九是目标机器没装对应的 Visual C++ 运行库。CefSharp 各版本依赖的 VC++ 运行库版本不同,43 这条线对应的是 2013 版。解决:把运行库安装包一并放进部署包,装完再启动;确认手段是用 Process Monitor 过滤libcef.dll的Load Image事件,看它在哪个路径上返回了NAME NOT FOUND。

第三条:窗口出来了,页面一片白,什么都不显示。先看cef.log,如果日志停在 “Requesting navigation” 之后就没有下文,通常是资源加载被拦了。常见的三个来源:杀毒软件的网页防护把本地 HTTP 请求掐了、页面本身依赖的接口超时、GPU 合成在特定显卡上卡死。排查顺序是:先Load("about:blank")确认 CEF 本身能初始化,再换成本地一个纯文本 HTML 文件确认渲染链路通,最后才怀疑网络和 GPU。确认是 GPU 问题就加disable-gpu开关,这个开关救过我不止一次。

第四条:中文全是乱码或者方块。直接检查两处:settings.Locale是否设成了zh-CN,以及输出目录的locales下有没有zh-CN.pak。只设 Locale 不拷 pak 文件是最常见的组合错误。另外AcceptLanguageList影响的是页面请求头里的Accept-Language,和后端返回什么语言的内容有关,跟界面乱码不是一回事,两个都要设。

第五条:JS 里调用桥对象报 undefined。两种可能:注册时机晚于页面加载,或者桥对象的方法签名里有 CefSharp 不支持的类型。前者的判断方法是打开 F12 控制台敲window.bridge,如果是undefined就往注册时机上查;如果对象存在但方法报错,就看方法参数里有没有DateTime、自定义类、泛型嵌套这些,改成字符串传递再在 C# 端解析。

5.3 打开 CEF 日志定位问题

cef.log是这套东西唯一的黑匣子。日志级别设成Info之后,能看到页面请求、进程启动、GPU 初始化这几条主线。几个值得记住的日志特征:

  • 出现Failed to create a Chromium subprocess—— 子进程 exe 缺失或被安全软件拦截
  • 出现GPU process launch failed—— 显卡驱动或者远程桌面环境导致,加 disable-gpu
  • 出现Unable to load locale—— 缺 pak 文件
  • 日志文件根本不存在 ——Cef.Initialize就没成功走到写日志那一步,问题在更前面

日志路径我固定在%LOCALAPPDATA%下而不是 exe 同级目录,原因是很多客户把程序装在Program Files里,普通用户权限写不进去,日志静默失败,排查时还以为程序没报错。

6. 进阶:本地页面加载、缓存目录与一个稳定收尾的写法

内嵌浏览器的业务系统,页面资源基本都是随程序分发的本地文件。直接用file:///加载会遇到跨域限制,XHR 请求本地 JSON 会被拦,localStorage的域也变得不可控。我一般起一个轻量的本地 HTTP 服务,把页面挂在一个固定的虚拟域名下,再配合请求拦截把域名映射到安装目录。

public class LocalResourceHandler : RequestHandler { private readonly string _root; public LocalResourceHandler(string root) { _root = root; } protected override bool OnBeforeBrowse(IWebBrowser browser, IRequest request, bool isRedirect, bool isMainFrame) { // 只放行虚拟域名,其余交给默认处理 return !request.Url.StartsWith("http://app.local/"); } public override bool ProcessRequest(IWebBrowser browser, IRequest request, out string redirectUrl, out bool isHandled) { redirectUrl = null; isHandled = false; if (!request.Url.StartsWith("http://app.local/")) return false; // 剥掉虚拟域名,映射到安装目录下的 web 文件夹 string relative = request.Url.Substring("http://app.local/".Length); string full = Path.Combine(_root, relative.Replace('/', '\\')); if (!File.Exists(full)) return false; // 这里只演示思路:实际用自定义 SchemeHandler 返回文件流 isHandled = true; return true; } }

接口签名在不同主版本之间有出入,ProcessRequest在较新的版本里已经被IResourceHandler体系取代。老版本上的稳妥做法是别自己造轮子,直接起一个本地 HTTP 监听,把http://127.0.0.1:端口/作为起始地址,页面资源、接口代理一并走这个端口。这样做的好处是调试时可以用外部浏览器打开同一个地址对照,出问题时能立刻分清是 CEF 的锅还是页面自己的锅。

缓存目录这块,我给每个主版本单独一个子目录,比如Cache\43、Cache\49。不同 Chromium 版本的缓存格式不兼容,共用一个目录会在升级后出现各种诡异的加载失败;分目录之后,升级时旧目录留着不动,新版本自己建新的,回滚也方便。

最后说个习惯。这一类内嵌浏览器的项目,我每次改完配置都会强制走一遍固定流程:清bin和obj、重新生成、在没有装过任何运行库的干净虚拟机上跑一次、看cef.log有没有 warning、关窗确认进程完全退出。这五步加起来不到十分钟,但能挡掉我见过的绝大多数“开发机上好好的、客户那边起不来”。最开始我省掉这一步,结果一份部署包在客户现场卡了三天,最后发现只是少装了一个运行库。

这份示例代码的完整结构就是上面这几块:Program 里的初始化和清理、窗体里的控件装配和事件、桥对象的双向通信、以及部署时该拷的文件清单。把它当成一个骨架,把你自己的业务页面和接口填进去,剩下的就是调参数和看日志的耐心活。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进