人虽然渺小,人生虽然短促,但是人能学,人能修身,人能自我完善,人的可贵在人自身。
——杨绛
一、Xilium.CefGlue 简介
Xilium.CefGlue 是一个开源项目,它是基于 Chromium Embedded Framework (CEF) 的一个 .NET 绑定库。CEF 是一个强大的开源项目,它允许开发者在自己的应用程序中嵌入完整的 Chromium 浏览器引擎。
Xilium.CefGlue 提供了一个简单易用的方式,让开发者能够使用 C# 或其他 .NET 语言来创建基于 Chromium 的应用程序。它提供了对 CEF 的封装,使得开发者可以轻松地在自己的应用程序中嵌入一个功能强大的浏览器引擎。
使用 Xilium.CefGlue,开发者可以实现各种功能,例如创建自定义的浏览器窗口、加载网页、处理 JavaScript 和 DOM 事件、执行 JavaScript 代码等等。它还支持与原生代码的交互,可以通过 CEF 的扩展机制来扩展功能。
Xilium.CefGlue 的优点包括:
- 跨平台:CEF 支持多个操作系统,包括 Windows、Linux 和 macOS,因此 Xilium.CefGlue 也可以在这些平台上运行。
- 强大的功能:由于基于 Chromium,Xilium.CefGlue 提供了一个功能丰富的浏览器引擎,支持 HTML5、CSS3、JavaScript 等最新的 Web 技术。
- 易于使用:Xilium.CefGlue 提供了简单易用的 API,使得开发者可以快速上手并构建自己的应用程序。
- 社区支持:作为一个开源项目,Xilium.CefGlue 拥有活跃的社区,开发者可以从社区中获取支持和贡献代码。
二、参考资料
Xilium.CefGlue 文档不多,可以参照 CEF 官网文档,
chromiumembedded / cef / wiki / GeneralUsage — Bitbuckethttps://bitbucket.org/chromiumembedded/cef/wiki/GeneralUsagechromiumembedded / cef / wiki / JavaScriptIntegration — Bitbucket
https://bitbucket.org/chromiumembedded/cef/wiki/JavaScriptIntegration.md
三、项目源代码下载
后续实现的所有 Demo 都基于官方项目源码上修改调整,
# 必应搜索关键字 CefGlue wiki
# 官网 https://xilium.bitbucket.io/cefglue/
# api文档 https://xilium.bitbucket.io/cefglue/doc/index.html
# cef-builds https://cef-builds.spotifycdn.com/index.html
首先下载项目源代码,
# 指定分支 5615
# git clone -b 5615 https://gitlab.com/xiliumhq/chromiumembedded/cefglue.git
# 默认 main
git https://gitlab.com/xiliumhq/chromiumembedded/cefglue.git
查看对应 CEF 版本,
# CefGlue\Interop\version.g.cs
# CEF_VERSION
然后下载对应版本的CEF,
如果是 Debug 编译模式,则需要将 Debug 与 Resources 这两个文件夹下的所有内容拷贝到对应的 bin 文件夹,
如果是 Release 编译模式,则需要将 Release 与 Resources 这两个文件夹下的所有内容拷贝到对应的bin文件夹,
四、启动 Xilium.CefGlue.Client
Xilium.CefGlue.Client 项目设为启动项,Program.cs 注释掉以下这行,
#必须使用 Release 模式运行,Debug 模式会白屏闪退,
//BrowserSubprocessPath = browserProcessPath,
这里说明一下,CefApp 通常需要一个主进程(Browser Process)和一个子进程(Render Process)运行,主进程负责管控窗口生命周期,子进程负责管控Web生命周期,所有网页资源的加载、js脚本都会在子进程中执行,这样设计的好处就是,就算子进程崩溃,也不会影响主应用的运行。当 BrowserSubprocessPath 未指定子进程路径值时,CefApp 则以当前主进程路径作为默认值。
访问必应官网效果如下,
在 Debug 模式下黑屏闪退的问题,我们可以通过日志查看具体原因,
var settings = new CefSettings
{
//BrowserSubprocessPath = browserProcessPath,
MultiThreadedMessageLoop = true,
LogSeverity = CefLogSeverity.Error,
LogFile = "CefGlue.log",
};
报错日志如下,
[0130/153649.007:FATAL:scoped_com_initializer.cc(56)] Check failed: ((HRESULT)0x80010106L) != hr_ (-2147417850 vs. -2147417850)Invalid COM thread model change
[0130/153651.532:ERROR:gpu_process_host.cc(952)] GPU process exited unexpectedly: exit_code=-2147483645
[0130/153651.800:ERROR:ssl_client_socket_impl.cc(985)] handshake failed; returned -1, SSL error code 1, net_error -103
[0130/153653.256:FATAL:scoped_com_initializer.cc(56)] Check failed: ((HRESULT)0x80010106L) != hr_ (-2147417850 vs. -2147417850)Invalid COM thread model change
[0130/153655.150:ERROR:gpu_process_host.cc(952)] GPU process exited unexpectedly: exit_code=-2147483645
[0130/153656.686:FATAL:scoped_com_initializer.cc(56)] Check failed: ((HRESULT)0x80010106L) != hr_ (-2147417850 vs. -2147417850)Invalid COM thread model change
[0130/153658.534:ERROR:gpu_process_host.cc(952)] GPU process exited unexpectedly: exit_code=-2147483645
[0130/153659.936:FATAL:scoped_com_initializer.cc(56)] Check failed: ((HRESULT)0x80010106L) != hr_ (-2147417850 vs. -2147417850)Invalid COM thread model change
[0130/153701.879:ERROR:gpu_process_host.cc(952)] GPU process exited unexpectedly: exit_code=-2147483645
[0130/153703.454:FATAL:scoped_com_initializer.cc(56)] Check failed: ((HRESULT)0x80010106L) != hr_ (-2147417850 vs. -2147417850)Invalid COM thread model change
[0130/153705.239:ERROR:gpu_process_host.cc(952)] GPU process exited unexpectedly: exit_code=-2147483645
[0130/153706.763:FATAL:scoped_com_initializer.cc(56)] Check failed: ((HRESULT)0x80010106L) != hr_ (-2147417850 vs. -2147417850)Invalid COM thread model change
[0130/153708.542:ERROR:gpu_process_host.cc(952)] GPU process exited unexpectedly: exit_code=-2147483645
[0130/153710.003:FATAL:scoped_com_initializer.cc(56)] Check failed: ((HRESULT)0x80010106L) != hr_ (-2147417850 vs. -2147417850)Invalid COM thread model change
[0130/153711.792:ERROR:gpu_process_host.cc(952)] GPU process exited unexpectedly: exit_code=-2147483645
[0130/153713.272:FATAL:scoped_com_initializer.cc(56)] Check failed: ((HRESULT)0x80010106L) != hr_ (-2147417850 vs. -2147417850)Invalid COM thread model change
[0130/153715.113:ERROR:gpu_process_host.cc(952)] GPU process exited unexpectedly: exit_code=-2147483645
[0130/153716.724:FATAL:scoped_com_initializer.cc(56)] Check failed: ((HRESULT)0x80010106L) != hr_ (-2147417850 vs. -2147417850)Invalid COM thread model change
[0130/153718.525:ERROR:gpu_process_host.cc(952)] GPU process exited unexpectedly: exit_code=-2147483645
[0130/153718.525:FATAL:gpu_data_manager_impl_private.cc(440)] GPU process isn't usable. Goodbye.
日志中可以看出,GPU 进程不可用,导致了黑屏,这跟之前说到的子进程是相关的,如此看来,Debug模式下,必须要指定 BrowserSubprocessPath 子进程参数,
browserProcessPath = @"D:\AwsomeWorkSpace\cefglue-5481\CefGlue.Demo.WinForms\bin\x64\Release\net5.0-windows\Xilium.CefGlue.Demo.WinForms.exe";
var settings = new CefSettings
{
BrowserSubprocessPath = browserProcessPath,
MultiThreadedMessageLoop = true,
LogSeverity = CefLogSeverity.Error,
LogFile = "CefGlue.log",
};
可以看到,指定了子进程之后,能够使用 Debug 模式启动项目了,
五、JS 调用 C#
1、CefRuntime.RegisterExtension
定义 DemoRenderProcessHandler ,该类继承 CefRenderProcessHandler,重写 OnWebKitInitialized,通过 CefRuntime.RegisterExtension 注册 C# 方法,
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Xilium.CefGlue.Client.Handlers
{
public class DemoRenderProcessHandler : CefRenderProcessHandler
{
/// <summary>
/// 自定义 CefV8Handler
/// </summary>
private DemoCefV8Handler _demoCefV8Handler;
protected override void OnWebKitInitialized()
{
_demoCefV8Handler = new DemoCefV8Handler();
// js 注册 C# 方法 MyFunction
const string javascriptCode = @"function demoCefV8Handler() {};
if (!demoCefV8Handler) demoCefV8Handler = {};
(function() {
demoCefV8Handler.myFunction = function(arg0) {
native function MyFunction(arg0);
return MyFunction(arg0);
};
})();";
// 注册 js 拓展
CefRuntime.RegisterExtension("v8/myFunction", javascriptCode, _demoCefV8Handler);
base.OnWebKitInitialized();
}
}
}
定义一个 DemoCefV8Handler ,该类继承CefV8Handler,重写 Execute 方法,
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Xilium.CefGlue.Client.Handlers
{
public class DemoCefV8Handler : CefV8Handler
{
/// <summary>
/// 默认构造函数
/// </summary>
public DemoCefV8Handler()
{
}
protected override bool Execute(string name, CefV8Value obj, CefV8Value[] arguments, out CefV8Value returnValue, out string exception)
{
string result = string.Empty;
switch (name)
{
case "MyFunction":
result = string.Format("MyFunction( arg ) => {0}", arguments[0].GetStringValue());
break;
default:
break;
}
returnValue = CefV8Value.CreateString(result);
exception = null;
return true;
}
}
}
定义 DemoApp ,该类继承 CefApp,重写 CefRenderProcessHandler 方法,
namespace Xilium.CefGlue.Client
{
using System;
using System.Collections.Generic;
using System.Text;
using Xilium.CefGlue;
using Xilium.CefGlue.Client.Handlers;
internal sealed class DemoApp : CefApp
{
protected override void OnBeforeCommandLineProcessing(string processType, CefCommandLine commandLine)
{
;
}
private CefRenderProcessHandler _renderProcessHandler = new DemoRenderProcessHandler();
protected override CefRenderProcessHandler GetRenderProcessHandler()
{
return _renderProcessHandler;
}
}
}
js 调用 ,
const msg = window.demoCefV8Handler.myFunction('url');
补充说明,以上 demo 是 Xilium.CefGlue.Client 以 Release 模式运行,并没有指定子进程 BrowserSubprocessPath 。而在指定子进程参数时,必须在子进程注册 C# 方法,在主进程中注册C# 方法是不生效的,因为所有网页资源的加载、js脚本都在子进程中执行。
例如,Client 作为主进程,指定 BrowserSubprocessPath 为 Demo.WinForms,在 Demo.WinForms 中注册 C# 方法,则运行效果如下,
注:这种方式将预定义的 JS 代码注册到 context 中,一旦注册,不可修改。
2、CefV8Context
/// <summary>
/// Create a new CefV8Value object of type function. This method should only
/// be called from within the scope of a CefRenderProcessHandler, CefV8Handler
/// or CefV8Accessor callback, or in combination with calling Enter() and
/// Exit() on a stored CefV8Context reference.
/// </summary>
public static CefV8Value CreateFunction(string name, CefV8Handler handler)
namespace Xilium.CefGlue.Demo
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using Xilium.CefGlue;
using Xilium.CefGlue.Demo.Handlers;
using Xilium.CefGlue.Wrapper;
class DemoRenderProcessHandler : CefRenderProcessHandler
{
public DemoRenderProcessHandler()
{
_demoCefV8Handler = new DemoCefV8Handler();
}
private DemoCefV8Handler _demoCefV8Handler;
protected override void OnContextCreated(CefBrowser browser, CefFrame frame, CefV8Context context)
{
var global = context.GetGlobal();
var extent = CefV8Value.CreateFunction("f1", _demoCefV8Handler);
global.SetValue("f1", extent, CefV8PropertyAttribute.None);
// MessageRouter.OnContextCreated doesn't capture CefV8Context immediately,
// so we able to release it immediately in this call.
context.Dispose();
}
}
}
// js 调用
const msg = window.f1('f1');
效果,
注:这种方式在当前 frame 创建 context 时将 C# 方法绑定到 window 对象,每次 frame 重新加载时,都会重新执行绑定过程,
六、C# 调用 JS
1、CefFrame.ExecuteJavaScript
/// <summary>
/// Execute a string of JavaScript code in this frame. The |script_url|
/// parameter is the URL where the script in question can be found, if any.
/// The renderer may request this URL to show the developer the source of the
/// error. The |start_line| parameter is the base line number to use for
/// error reporting.
/// </summary>
public void ExecuteJavaScript(string code, string url, int line)
// MainForm.cs
// ...
var browser = new CefWebBrowser();
// ...
// browser 加载完成后弹窗
browser.LoadEnd += (s, e) =>
{
var mainFrame = browser.Browser.GetMainFrame();
mainFrame.ExecuteJavaScript("alert('hello world');", mainFrame.Url, 0);
};
// ...
效果,
2、CefV8Value.ExecuteFunction
该方法用于注册 JS 函数回调,使用说明如下,
/// <summary>
/// Execute the function using the current V8 context. This method should only
/// be called from within the scope of a CefV8Handler or CefV8Accessor
/// callback, or in combination with calling Enter() and Exit() on a stored
/// CefV8Context reference. |object| is the receiver ('this' object) of the
/// function. If |object| is empty the current context's global object will be
/// used. |arguments| is the list of arguments that will be passed to the
/// function. Returns the function return value on success. Returns NULL if
/// this method is called incorrectly or an exception is thrown.
/// </summary>
public CefV8Value ExecuteFunction(CefV8Value obj, CefV8Value[] arguments)
在 Render Process 绑定方法 register ,重写 RenderProcessHandler 的 OnContextCreated 方法,将 register 方法绑定到 window,我们就可以在前端绑定 js 回调(匿名 js 方法、带返回值的 js 方法);也重写了 OnProcessMessageReceived 方法,模拟这样一个场景:经过一番 IPC 通讯后,在某个条件/节点下执行 js 回调,
// DemoRenderProcessHandler.cs
// ...
/// <summary>
/// 定义 CallbackMap
/// </summary>
public static Dictionary<Tuple<string, int>, Tuple<CefV8Context, CefV8Value>> callbackMap = new Dictionary<Tuple<string, int>, Tuple<CefV8Context, CefV8Value>>();
protected override void OnContextCreated(CefBrowser browser, CefFrame frame, CefV8Context context)
{
var global = context.GetGlobal();
var myExtent = CefV8Value.CreateFunction("register", new MyV8Handler());
global.SetValue("register", myExtent, CefV8PropertyAttribute.None);
//MessageRouter.OnContextCreated doesn't capture CefV8Context immediately,
// so we able to release it immediately in this call.
context.Dispose();
}
protected override bool OnProcessMessageReceived(CefBrowser browser, CefFrame frame, CefProcessId sourceProcess, CefProcessMessage message)
{
// Execute the registered JavaScript callback if any.
if (callbackMap.Count > 0)
{
string messageName = message.Name;
Tuple<string, int> key = Tuple.Create(messageName, browser.Identifier);
if (callbackMap.ContainsKey(key))
{
Tuple<CefV8Context, CefV8Value> value = callbackMap[key];
CefV8Context context = value.Item1;
CefV8Value callback = value.Item2;
// Enter the context.
context.Enter();
List<CefV8Value> arguments = new List<CefV8Value>();
// First argument is the message name.
arguments.Add(CefV8Value.CreateString(messageName));
// Second argument is the list of message arguments.
CefV8ValueHelper.SetList(message.Arguments, arguments);
// Execute the callback.
CefV8Value retval = callback.ExecuteFunction(null, arguments.ToArray());
if (!retval.IsNull)
{
if (retval.IsBool)
{
return retval.GetBoolValue();
}
}
// Exit the context.
context.Exit();
}
return true;
}
return false;
}
// ...
using System.Collections.Generic;
namespace Xilium.CefGlue.Demo.Helper
{
public class CefV8ValueHelper
{
/// <summary>
/// SetList function to convert CefListValue to CefV8Value.
/// </summary>
/// <param name="list"></param>
/// <param name="args"></param>
public static void SetList(CefListValue? list, List<CefV8Value> args)
{
for (int i = 0; i < list?.Count; i++)
{
CefValueType cefValueType = list.GetValueType(i);
switch (cefValueType)
{
case CefValueType.Bool:
{
args.Add(CefV8Value.CreateBool(list.GetBool(i)));
break;
}
case CefValueType.Double:
{
args.Add(CefV8Value.CreateDouble(list.GetDouble(i)));
break;
}
case CefValueType.Int:
{
args.Add(CefV8Value.CreateInt(list.GetInt(i)));
break;
}
case CefValueType.Null:
{
args.Add(CefV8Value.CreateNull());
break;
}
case CefValueType.String:
{
args.Add(CefV8Value.CreateString(list.GetString(i)));
break;
}
default: {
args.Add(CefV8Value.CreateUndefined());
break;
}
}
}
}
}
}
自定义实现 CefV8Handler 的 Execute 方法,将 js 回调及其上下文保存到 callbackMap 中,
using System;
namespace Xilium.CefGlue.Demo.Handlers
{
public class MyV8Handler : CefV8Handler
{
protected override bool Execute(string name, CefV8Value obj, CefV8Value[] arguments, out CefV8Value returnValue, out string exception)
{
if (name.Equals("register"))
{
if (arguments.Length == 2 && arguments[0].IsString && arguments[1].IsFunction)
{
string messageName = arguments[0].GetStringValue();
CefV8Context context = CefV8Context.GetCurrentContext();
int browserId = context.GetBrowser().Identifier;
Tuple<string, int> key = Tuple.Create(messageName, browserId);
Tuple<CefV8Context, CefV8Value> value = Tuple.Create(context, arguments[1]);
DemoRenderProcessHandler.callbackMap.Add(key, value);
returnValue = CefV8Value.CreateString($"register callback function {messageName} done.");
exception = null;
return true;
}
}
returnValue = null;
exception = null;
return false;
}
}
}
Browser Process 模拟触发 js 回调,在网页加载完成后 3 秒触发 js 回调,
// MainForm.cs
browser.LoadEnd += (s, e) =>
{
BeginInvoke(new Action(()=> {
Thread.Sleep(3000);
var mainFrame = browser.Browser.GetMainFrame();
var message = CefProcessMessage.Create("binding_test");
message.Arguments?.SetString(0, "yushanma");
message.Arguments?.SetString( 1, "余衫马");
mainFrame.SendProcessMessage(CefProcessId.Renderer, message);
}));
};
前端页面调用如下,通过匿名 js 回调,携带返回数据显示到页面上,
<!doctype html>
<html lang="zh" data-server-rendered="true">
<head>
<title>Demo</title>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
</head>
<body>
<div>
<div id="log"></div>
<div id="result"></div>
</div>
<script>
// In JavaScript register the callback function.
const result = window.register('binding_test', function(arg0, arg1, arg2) {
document.getElementById('result').innerText = `Response: arg0 => ${arg0}, arg1 => ${arg1} ,arg2 => ${arg2}`;
});
// 获取 div 元素,修改文本值
document.getElementById("log").innerText = result;
</script>
</body>
</html>
效果,
3、CefV8Value.ExecuteFunctionWithContext
该方法用于注册 JS 函数回调并在指定的上下文中执行,使用说明如下,
/// <summary>
/// Execute the function using the specified V8 context. |object| is the
/// receiver ('this' object) of the function. If |object| is empty the
/// specified context's global object will be used. |arguments| is the list of
/// arguments that will be passed to the function. Returns the function return
/// value on success. Returns NULL if this method is called incorrectly or an
/// exception is thrown.
/// </summary>
public CefV8Value ExecuteFunctionWithContext(CefV8Context context, CefV8Value obj, CefV8Value[] arguments)
在 Render Process 绑定方法 register ,
// DemoRenderProcessHandler.cs
protected override void OnContextCreated(CefBrowser browser, CefFrame frame, CefV8Context context)
{
var global = context.GetGlobal();
var myExtent = CefV8Value.CreateFunction("register", new MyV8Handler());
global.SetValue("register", myExtent, CefV8PropertyAttribute.None);
//MessageRouter.OnContextCreated doesn't capture CefV8Context immediately,
// so we able to release it immediately in this call.
context.Dispose();
}
从 arguments 参数中获取 js 回调,可以立即执行或者延迟执行 ExecuteFunctionWithContext 方法来回调 js 脚本,
namespace Xilium.CefGlue.Demo.Handlers
{
public class MyV8Handler : CefV8Handler
{
/// <summary>
/// js 回调
/// </summary>
private CefV8Value _callback_func;
/// <summary>
/// 回调上下文
/// </summary>
private CefV8Context _callback_context;
protected override bool Execute(string name, CefV8Value obj, CefV8Value[] arguments, out CefV8Value returnValue, out string exception)
{
if (name.Equals("register"))
{
if (arguments.Length== 1 && arguments[0].IsFunction )
{
_callback_func = arguments[0];
_callback_context = CefV8Context.GetCurrentContext();
returnValue = _callback_func.ExecuteFunctionWithContext(_callback_context, null, arguments);
if (returnValue.IsNull)
{
exception = returnValue.GetException().Message;
return false;
}
returnValue = CefV8Value.CreateInt(returnValue.GetIntValue() + 2);
exception = null;
return true;
}
}
returnValue = null;
exception = null;
return false;
}
}
}
在 js 中 注册回调方法,
function myCallbackFunc() {
return 0;
}
alert(`myCallbackFunc result : ${window.register(myCallbackFunc)}`);
效果,
七、进程间通讯(IPC)
1、browser process -> render process
从 browser process 发送到 render process 的信息在 CefRenderProcessHandler::OnProcessMessageReceived() 方法中接收,
在 browser process 中,CefWebBrowser 加载网页结束后,将 js 脚本发送到 render process ,
// MainForm.cs
// ...
var browser = new CefWebBrowser();
// 注入自定义标题栏
browser.LoadEnd += (s, e) =>
{
var mainFrame = browser.Browser.GetMainFrame();
// 不可执行, This method can only be called from the render process.
//mainFrame.V8Context.TryEval("return 1 + 1;", mainFrame.Url, 0, out CefV8Value? returnValue, out CefV8Exception? exception);
// 解决:发送给 Renderer Process 处理
var message = CefProcessMessage.Create("ExecuteJavaScript");
message.Arguments?.SetString(0, "1 + 1");
mainFrame.SendProcessMessage(CefProcessId.Renderer, message);
};
// ...
在 render process 中接收并执行 js 脚本,将执行结果返回 browser process,
// DemoRenderProcessHandler.cs
protected override bool OnProcessMessageReceived(CefBrowser browser, CefFrame frame, CefProcessId sourceProcess, CefProcessMessage message)
{
if (message.Name == "ExecuteJavaScript")
{
var code = message.Arguments?.GetString(0);
var mainFrame = browser.GetMainFrame();
// 执行JavaScript代码
var result = mainFrame.V8Context.TryEval(code ?? "", mainFrame.Url, 0, out var returnValue, out var exception);
// 将结果发送回Browser进程
var response = CefProcessMessage.Create("JavaScriptResult");
response.Arguments?.SetBool(0, result);
response.Arguments?.SetInt(1, returnValue?.GetIntValue() ?? 0);
response.Arguments?.SetString(2, exception?.Message ?? string.Empty);
mainFrame.SendProcessMessage(CefProcessId.Browser, response);
return true;
}
}
2、render process -> browser process
从 render process 发送到 browser process 的信息在 CefClient::OnProcessMessageReceived() 方法中接收,
// CefWebClient.cs
protected override bool OnProcessMessageReceived(CefBrowser browser, CefFrame frame, CefProcessId sourceProcess, CefProcessMessage message)
{
if (message.Name == "JavaScriptResult")
{
var result = message.Arguments?.GetBool(0) ?? false;
if (result)
{
frame.ExecuteJavaScript($"alert('1 + 1 = {message.Arguments?.GetInt(1)}');", frame.Url, 0);
return true;
}
frame.ExecuteJavaScript($"alert('{message.Arguments?.GetString(2)}');", frame.Url, 0);
return true;
}
return false;
}
通过 IPC 机制,我们可以执行 JS 脚本,并接收 JS 执行结果,效果如下,
注意:接收参数的类型、次序与发送参数的类型、次序必须保持一致,否则无法解析正确的数据,
3、Message Router
// The below classes implement support for routing aynchronous messages between
// JavaScript running in the renderer process and C++ running in the browser
// process. An application interacts with the router by passing it data from
// standard CEF C++ callbacks (OnBeforeBrowse, OnProcessMessageReceived,
// OnContextCreated, etc). The renderer-side router supports generic JavaScript
// callback registration and execution while the browser-side router supports
// application-specific logic via one or more application-provided Handler
// instances.
//
// The renderer-side router implementation exposes a query function and a cancel
// function via the JavaScript 'window' object:
//
// // Create and send a new query.
// var request_id = window.cefQuery({
// request: 'my_request',
// persistent: false,
// onSuccess: function(response) {},
// onFailure: function(error_code, error_message) {}
// });
//
// // Optionally cancel the query.
// window.cefQueryCancel(request_id);
//
// When |window.cefQuery| is executed the request is sent asynchronously to one
// or more C++ Handler objects registered in the browser process. Each C++
// Handler can choose to either handle or ignore the query in the
// Handler::OnQuery callback. If a Handler chooses to handle the query then it
// should execute Callback::Success when a response is available or
// Callback::Failure if an error occurs. This will result in asynchronous
// execution of the associated JavaScript callback in the renderer process. Any
// queries unhandled by C++ code in the browser process will be automatically
// canceled and the associated JavaScript onFailure callback will be executed
// with an error code of -1.
//
// Queries can be either persistent or non-persistent. If the query is
// persistent then the callbacks will remain registered until one of the
// following conditions are met:
//
// A. The query is canceled in JavaScript using the |window.cefQueryCancel|
// function.
// B. The query is canceled in C++ code using the Callback::Failure function.
// C. The context associated with the query is released due to browser
// destruction, navigation or renderer process termination.
//
// If the query is non-persistent then the registration will be removed after
// the JavaScript callback is executed a single time. If a query is canceled for
// a reason other than Callback::Failure being executed then the associated
// Handler's OnQueryCanceled method will be called.
//
// Some possible usage patterns include:
//
// One-time Request. Use a non-persistent query to send a JavaScript request.
// The Handler evaluates the request and returns the response. The query is
// then discarded.
//
// Broadcast. Use a persistent query to register as a JavaScript broadcast
// receiver. The Handler keeps track of all registered Callbacks and executes
// them sequentially to deliver the broadcast message.
//
// Subscription. Use a persistent query to register as a JavaScript subscription
// receiver. The Handler initiates the subscription feed on the first request
// and delivers responses to all registered subscribers as they become
// available. The Handler cancels the subscription feed when there are no
// longer any registered JavaScript receivers.
//
// Message routing occurs on a per-browser and per-context basis. Consequently,
// additional application logic can be applied by restricting which browser or
// context instances are passed into the router. If you choose to use this
// approach do so cautiously. In order for the router to function correctly any
// browser or context instance passed into a single router callback must then
// be passed into all router callbacks.
//
// There is generally no need to have multiple renderer-side routers unless you
// wish to have multiple bindings with different JavaScript function names. It
// can be useful to have multiple browser-side routers with different client-
// provided Handler instances when implementing different behaviors on a per-
// browser basis.
//
// This implementation places no formatting restrictions on payload content.
// An application may choose to exchange anything from simple formatted
// strings to serialized XML or JSON data.
//
//
// EXAMPLE USAGE
//
// 1. Define the router configuration. You can optionally specify settings
// like the JavaScript function names. The configuration must be the same in
// both the browser and renderer processes. If using multiple routers in the
// same application make sure to specify unique function names for each
// router configuration.
//
// // Example config object showing the default values.
// CefMessageRouterConfig config;
// config.js_query_function = "cefQuery";
// config.js_cancel_function = "cefQueryCancel";
//
// 2. Create an instance of CefMessageRouterBrowserSide in the browser process.
// You might choose to make it a member of your CefClient implementation,
// for example.
//
// browser_side_router_ = CefMessageRouterBrowserSide::Create(config);
//
// 3. Register one or more Handlers. The Handler instances must either outlive
// the router or be removed from the router before they're deleted.
//
// browser_side_router_->AddHandler(my_handler);
//
// 4. Call all required CefMessageRouterBrowserSide methods from other callbacks
// in your CefClient implementation (OnBeforeClose, etc). See the
// CefMessageRouterBrowserSide class documentation for the complete list of
// methods.
//
// 5. Create an instance of CefMessageRouterRendererSide in the renderer
// process.
// You might choose to make it a member of your CefApp implementation, for
// example.
//
// renderer_side_router_ = CefMessageRouterRendererSide::Create(config);
//
// 6. Call all required CefMessageRouterRendererSide methods from other
// callbacks in your CefRenderProcessHandler implementation
// (OnContextCreated, etc). See the CefMessageRouterRendererSide class
// documentation for the complete list of methods.
//
// 7. Execute the query function from JavaScript code.
//
// window.cefQuery({request: 'my_request',
// persistent: false,
// onSuccess: function(response) { print(response); },
// onFailure: function(error_code, error_message) {} });
//
// 8. Handle the query in your Handler::OnQuery implementation and execute the
// appropriate callback either immediately or asynchronously.
//
// void MyHandler::OnQuery(int64_t query_id,
// CefRefPtr<CefBrowser> browser,
// CefRefPtr<CefFrame> frame,
// const CefString& request,
// bool persistent,
// CefRefPtr<Callback> callback) {
// if (request == "my_request") {
// callback->Continue("my_response");
// return true;
// }
// return false; // Not handled.
// }
//
// 9. Notice that the onSuccess callback is executed in JavaScript.
Xilium.CefGlue.Demo.WinForms 项目中实现了该 Demo,我们直接放开注释代码即可,全局搜索关键字,
DemoApp.BrowserMessageRouter
EXAMPLE USAGE 中的步骤 1(定义路由配置)、2(在 Browser Process 中实例化CefMessageRouterBrowserSide)、3(为CefMessageRouterBrowserSide实例添加一个Handler)、8(自定义实现 Handler) 在 DemoApp 中实现,
namespace Xilium.CefGlue.Demo
{
using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
using Xilium.CefGlue.Wrapper;
using System.Threading;
public abstract class DemoApp : IDisposable
{
public static CefMessageRouterBrowserSide BrowserMessageRouter { get; private set; }
// ...
private void RegisterMessageRouter()
{
if (!CefRuntime.CurrentlyOn(CefThreadId.UI))
{
PostTask(CefThreadId.UI, this.RegisterMessageRouter);
return;
}
// window.cefQuery({ request: 'my_request', onSuccess: function(response) { console.log(response); }, onFailure: function(err,msg) { console.log(err, msg); } });
DemoApp.BrowserMessageRouter = new CefMessageRouterBrowserSide(new CefMessageRouterConfig());
DemoApp.BrowserMessageRouter.AddHandler(new DemoMessageRouterHandler());
}
private class DemoMessageRouterHandler : CefMessageRouterBrowserSide.Handler
{
public override bool OnQuery(CefBrowser browser, CefFrame frame, long queryId, string request, bool persistent, CefMessageRouterBrowserSide.Callback callback)
{
if (request == "wait5")
{
new Thread(() =>
{
Thread.Sleep(5000);
callback.Success("success! responded after 5 sec timeout."); // TODO: at this place crash can occurs, if application closed
}).Start();
return true;
}
if (request == "wait5f")
{
new Thread(() =>
{
Thread.Sleep(5000);
callback.Failure(12345, "success! responded after 5 sec timeout. responded as failure.");
}).Start();
return true;
}
if (request == "wait30")
{
new Thread(() =>
{
Thread.Sleep(30000);
callback.Success("success! responded after 30 sec timeout.");
}).Start();
return true;
}
if (request == "noanswer")
{
return true;
}
var chars = request.ToCharArray();
Array.Reverse(chars);
var response = new string(chars);
callback.Success(response);
return true;
}
public override void OnQueryCanceled(CefBrowser browser, CefFrame frame, long queryId)
{
}
}
// ...
}
EXAMPLE USAGE 中的步骤 4(从其他回调中调用所有必需的 CefMessageRouterBrowserSide 方法) 分別在 WebLifeSpanHandler 、WebRequestHandler 、WebClient 中实现,
namespace Xilium.CefGlue.Demo.Browser
{
using System;
using System.Collections.Generic;
using System.Text;
using Xilium.CefGlue;
internal sealed class WebLifeSpanHandler : CefLifeSpanHandler
{
// ...
protected override void OnBeforeClose(CefBrowser browser)
{
DemoApp.BrowserMessageRouter.OnBeforeClose(browser);
}
// ...
}
}
namespace Xilium.CefGlue.Demo.Browser
{
using System;
using System.Collections.Generic;
using System.Text;
class WebRequestHandler : CefRequestHandler
{
// ...
protected override bool OnBeforeBrowse(CefBrowser browser, CefFrame frame, CefRequest request, bool userGesture, bool isRedirect)
{
DemoApp.BrowserMessageRouter.OnBeforeBrowse(browser, frame);
return base.OnBeforeBrowse(browser, frame, request, userGesture, isRedirect);
}
protected override void OnRenderProcessTerminated(CefBrowser browser, CefTerminationStatus status)
{
DemoApp.BrowserMessageRouter.OnRenderProcessTerminated(browser);
}
// ...
}
}
namespace Xilium.CefGlue.Demo.Browser
{
using System;
using System.Collections.Generic;
using System.Text;
using Xilium.CefGlue;
internal sealed class WebClient : CefClient
{
// ...
protected override bool OnProcessMessageReceived(CefBrowser browser, CefFrame frame, CefProcessId sourceProcess, CefProcessMessage message)
{
// ...
var handled = DemoApp.BrowserMessageRouter.OnProcessMessageReceived(browser, frame, sourceProcess, message);
if (handled) return true;
// ...
}
// ...
}
}
EXAMPLE USAGE 中的步骤 5(在 Renderer Process 中实例化 CefMessageRouterRendererSide)、6(从其他回调中调用所有必需的 CefMessageRouterRendererSide 方法) 在 DemoRenderProcessHandler 中实现,
namespace Xilium.CefGlue.Demo
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using Xilium.CefGlue;
using Xilium.CefGlue.Demo.Handlers;
using Xilium.CefGlue.Wrapper;
class DemoRenderProcessHandler : CefRenderProcessHandler
{
// ...
public DemoRenderProcessHandler()
{
MessageRouter = new CefMessageRouterRendererSide(new CefMessageRouterConfig());
}
internal CefMessageRouterRendererSide MessageRouter { get; private set; }
protected override void OnContextCreated(CefBrowser browser, CefFrame frame, CefV8Context context)
{
MessageRouter.OnContextCreated(browser, frame, context);
//MessageRouter.OnContextCreated doesn't capture CefV8Context immediately,
// so we able to release it immediately in this call.
context.Dispose();
}
protected override void OnContextReleased(CefBrowser browser, CefFrame frame, CefV8Context context)
{
// MessageRouter.OnContextReleased releases captured CefV8Context (if have).
MessageRouter.OnContextReleased(browser, frame, context);
// Release CefV8Context.
context.Dispose();
}
protected override bool OnProcessMessageReceived(CefBrowser browser, CefFrame frame, CefProcessId sourceProcess, CefProcessMessage message)
{
// ...
var handled = MessageRouter.OnProcessMessageReceived(browser, sourceProcess, message);
if (handled) return true;
// ...
return false;
}
// ...
}
}
EXAMPLE USAGE 中的步骤 7,在前端 js 中调用 window.cefQuery ,
// js 调用
window.cefQuery({
request: 'wait5',
persistent: false,
onSuccess: function (response) {
console.log(response);
},
onFailure: function (err, msg) {
console.log(err, msg);
}
});
效果,
期间遇到一个大坑,一直报错空指针异常,
object reference not set to an instance of an object
降一下版本就能解决问题,
cefglue-5481
CEF 110.0.29+gfde72b5+chromium-110.0.5481.104