我的开源项目分享-基于SharpBrowser二次开发的定制浏览器,过程中填坑无数
大家好,我是热衷于开源技术的一名开发者。今天我想和大家分享一个我最近完成的项目——基于 SharpBrowser 二次开发的定制浏览器。SharpBrowser 是一个基于 C# 和 WinForms 的开源浏览器项目,它使用了 CefSharp(Chromium Embedded Framework 的 .NET 封装)作为内核,因此具备现代浏览器的核心能力。然而,在进行二次开发的过程中,我踩了无数的坑,从编译环境的配置到浏览器内核的定制,再到 UI 的调整,每一步都充满了挑战。今天这篇文章,我将以循序渐进的方式,从基础概念讲到高级用法,并附上完整的代码示例,希望能为同样有志于此的开发者提供一些参考。### 1. 基础概念:什么是 SharpBrowser 和 CefSharp?在动手之前,我们首先需要理解两个关键概念。-SharpBrowser:它是一个轻量级、快速且可扩展的开源浏览器,基于 .NET 框架。它的核心架构将 UI(使用 WinForms)与浏览器引擎(CefSharp)分离,这为我们进行二次开发提供了极大的便利。-CefSharp:它是 Chromium 内核的 .NET 封装。简单来说,它允许你在 C# 中嵌入一个完整的浏览器页面。我们定制浏览器,本质上就是控制这个嵌入式页面,并与之交互。为什么选择基于它二次开发?因为从头写一个浏览器引擎几乎是不可能的,而 SharpBrowser 提供了一个良好的起点,让我们可以专注于业务逻辑,而不是底层的网络协议、渲染、JavaScript 引擎等。### 2. 第一次填坑:环境搭建与编译这是我遇到的第一个大坑。SharpBrowser 的官方仓库默认是面向较旧的 .NET Framework 4.7.2 和特定的 CefSharp 版本。如果你直接拉取代码并尝试编译,大概率会失败,因为 NuGet 包可能无法还原,或者与你的 Visual Studio 版本不兼容。解决方案:手动升级项目到 .NET 6.0 或更高版本,并更新 CefSharp 到最新稳定版。这需要修改.csproj文件。下面是我的一个配置片段示例(已注释):xml<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>WinExe</OutputType> <TargetFramework>net6.0-windows</TargetFramework> <UseWindowsForms>true</UseWindowsForms> <!-- 关键:强制使用x64,因为CefSharp不支持AnyCPU --> <PlatformTarget>x64</PlatformTarget> <!-- 禁止生成与平台无关的程序集,避免运行时错误 --> <Prefer32Bit>false</Prefer32Bit> <!-- 自动绑定重定向,解决依赖冲突 --> <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects> <GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType> </PropertyGroup> <ItemGroup> <!-- 升级CefSharp.WinForms到最新稳定版本,例如 109.1.160 --> <PackageReference Include="CefSharp.WinForms" Version="109.1.160" /> <PackageReference Include="CefSharp.Common" Version="109.1.160" /> </ItemGroup></Project>填坑心得:不要害怕升级依赖。升级后,一些旧的 API 可能会被废弃,但 CefSharp 的文档和 IntelliSense 会帮助你修正。### 3. 深入核心:定制浏览器界面与行为SharpBrowser 默认的 UI 是传统的浏览器窗口(地址栏、导航按钮、标签页)。我的需求是将其改造成一个“信息看板”,即隐藏地址栏和标签页,只显示一个固定的网页,并支持自动刷新和自定义脚本注入。这需要修改MainForm.cs中的初始化逻辑。以下是我核心定制后的代码示例(带详细注释):csharp// 引入必要的命名空间using CefSharp;using CefSharp.WinForms;public partial class MainForm : Form{ private ChromiumWebBrowser _browser; // 浏览器控件实例 public MainForm() { InitializeComponent(); // 1. 初始化浏览器设置 var settings = new CefSettings(); // 设置缓存目录,避免每次启动都重新加载资源(填坑:必须设置,否则可能白屏) settings.CachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "MyCustomBrowser\\Cache"); // 禁用右键菜单,防止用户看到默认的“查看源代码”等选项 settings.ContextMenuHandler = new CustomContextMenuHandler(); Cef.Initialize(settings, performDependencyCheck: true, browserProcessHandler: null); // 2. 创建浏览器实例,加载一个固定的网址(例如内部仪表盘) _browser = new ChromiumWebBrowser("https://www.example.com/dashboard") { Dock = DockStyle.Fill // 填充整个窗体 }; // 3. 将浏览器控件添加到窗体 this.Controls.Add(_browser); // 4. 注册事件:等待页面加载完成后注入自定义JS _browser.LoadingStateChanged += OnLoadingStateChanged; } private void OnLoadingStateChanged(object sender, LoadingStateChangedEventArgs e) { // 需要回到UI线程操作 this.Invoke((MethodInvoker)delegate { // 当页面加载完成时 if (!e.IsLoading) { // 注入JavaScript,例如隐藏某个页脚或者自动点击某个按钮 string jsCode = @" // 隐藏页面底部的导航栏(假设有class为footer的元素) var footer = document.querySelector('.footer'); if (footer) { footer.style.display = 'none'; } // 自动点击一个ID为'load-more'的按钮 var loadMoreBtn = document.getElementById('load-more'); if (loadMoreBtn) { setTimeout(() => loadMoreBtn.click(), 2000); } "; // 执行异步JS,并处理结果 var script = _browser.EvaluateScriptAsync(jsCode); script.ContinueWith(t => { if (t.IsCompleted && t.Result.Success) { Console.WriteLine("脚本注入成功!"); } else { Console.WriteLine("脚本注入失败:" + t.Result?.Message); } }, TaskScheduler.FromCurrentSynchronizationContext()); } }); } // 自定义右键菜单处理器,阻止弹出任何菜单 public class CustomContextMenuHandler : IContextMenuHandler { public void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model) { // 清空所有菜单项 model.Clear(); } public bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags) { // 返回false表示不执行任何命令 return false; } public void OnContextMenuDismissed(IWebBrowser browserControl, IBrowser browser, IFrame frame) { // 方法体可以为空 } }}代码解读:- 我们通过CefSettings配置了缓存路径,这能显著提升二次启动速度。- 通过自定义IContextMenuHandler,我们屏蔽了浏览器默认的右键菜单,这对于一些需要锁定用户操作的应用场景非常有用。- 利用LoadingStateChanged事件,我们可以在页面加载完成后,动态注入 JavaScript,实现自动化操作。这是“定制”的精髓。### 4. 高级用法:与浏览器内页面的双向通信仅仅注入 JS 还不够,有时候我们需要从 C# 端调用页面里的 JavaScript 函数,或者从 JavaScript 调用 C# 方法。这需要实现IJavascriptObjectRepository或使用RegisterAsyncJsObject。示例:从C#调用JS函数,并传递参数csharp// 在浏览器加载前注册一个C#对象,供JS调用public class JsBridge{ public void ShowMessage(string message) { // 弹出一个Windows消息框,或者写入日志 MessageBox.Show(message); }}// 在初始化浏览器时:_browser.JavascriptObjectRepository.Register("bridge", new JsBridge(), isAsync: false);// 现在,在页面加载完成后,你可以注入JS去调用这个桥接对象private void CallJsFunction(){ // 调用页面中的自定义函数 myFunction string js = @" if (typeof myFunction === 'function') { myFunction('来自C#的问候'); } else { // 如果函数不存在,则通过C#桥接对象弹窗 bridge.ShowMessage('页面函数未找到'); } "; _browser.GetBrowser().MainFrame.ExecuteJavaScriptAsync(js);}填坑心得:在注册 JS 对象时,要注意线程安全。Register方法必须在Cef.Initialize之后,且在浏览器控件创建之前完成。此外,isAsync参数设置为false可以同步调用,但可能会导致 UI 卡顿,建议优先使用异步。### 5. 性能优化与稳定性在二次开发过程中,我遇到了内存泄漏和页面崩溃的问题。主要原因是没有正确处理浏览器控件的生命周期。关键点:在窗体关闭时,必须显式关闭浏览器控件,并调用Cef.Shutdown()。csharpprotected override void OnFormClosing(FormClosingEventArgs e){ base.OnFormClosing(e); // 释放浏览器资源 if (_browser != null) { _browser.Dispose(); _browser = null; } // 关闭Cef进程,否则程序可能无法完全退出 Cef.Shutdown();}### 总结基于 SharpBrowser 的二次开发是一次极具挑战但也收获满满的经历。从最初的编译失败,到后来实现页面的深度定制和双向通信,我不仅深入理解了 CefSharp 的架构,也提升了自己解决实际问题的能力。我的核心经验是:遇到问题不要慌,先检查版本兼容性,再查阅官方文档(CefSharp 的官方文档非常详细),最后动手写代码验证。希望我的分享能帮助你在开源定制的道路上少走弯路,也欢迎大家在评论区交流你们的填坑经历!