GeckoWebBrowser的使用及其前后端交互示例
最近项目中遇到一个需求,需要在一个winform程序中嵌入一个网页,并且可以互相操作(winform调用js, js调用winform)。
我一想,很简单啊,不是有现成的控件吗,那个叫 WebBrowser 的,怼上去不就完了?
殊不知,不登高山,不知天之高也;不临深溪,不知地之厚也...
这一搞,可就搞了一下午...
其实用法倒是挺简单,可是无奈我这几年被ES6的各种新语法惯坏了,调了半天,发现 WebBrowser 这个控件使用的是本机IE内核,并支持不了那么多高大上的写法。那咋办?客户机器上的IE那可真是五花八门,说不定现在还有 IE5.5 呢,要这么写js那得郁闷死啊。
没办法,那就换控件吧,网上搜一下看有没有现成的。
别说,立马找到一个 WebKit.NET
的东西,webkit内核,应该不错,值得信赖,一番试验...
最终并没有找到 x64 版本的,这意味着我们的程序只能以 x86 方式编译,不开心,放弃。
继续找,发现 nuget 里有一个 Geckofx60.64,一看就是 x64 版本啊,Any CPU 方式编译,棒棒的,Gecko大品牌,值得信任。好,就它了!
OK,絮叨完了,下面说正事儿。
STEP 1. 添加Nuget包引用
没啥可说的,nuget,引用就完了,包名:Geckofx60.64
.
引用成功后,项目中会增加一个名为 Firefox 的文件夹。
注意:FireFox 文件夹在编译时需要拷贝到 bin 文件夹中。
STEP 2. 在Form窗体上添加 GeckoWebBrowser 控件
首先假装我们在使用工具箱中自带的的WebBrowser,向Form窗体中添加WebBrowser控件。
然后打开 *.designer.cs 文件,找到 WebBrowser 控件声明,趁他不注意,将其类型从 WebBrower 修改为 GeckoWebBrowser
,如下:
// old
// private WebBrowser browser;
// new
private Gecko.GeckoWebBrowser browser;
STEP 3. GeckoWebBrowser 控件初始化
打开 Form1 的代码文件,在构造函数中添加代码:
/// <summary>
/// 构造
/// </summary>
public Form1()
{
InitializeComponent();
// 这里的 Firefox 其实是 STEP1 中的文件夹在 bin 中的相对路径
Xpcom.Initialize("Firefox");
browser.Dock = DockStyle.Fill;
}
STEP 4. 打开指定Url
没啥好说的,上代码
private void Form1_Load(object sender, EventArgs e)
{
browser.Navigate("http://127.0.0.1/hello.html");
}
STEP 5. 定义 JS 与 winform 的交互协议
因为没什么特殊要求,就简单定义了一下:
注意:因为 GeckoWebBrowser 自身原因,javascript 在调用后端 C# 方法之后,无法接收 C# 方法的返回值。基于这个原因,只好在 C# 方法执行完毕之前,再调用 javascript 方法[2],传入执行结果,前端只能通过 javascript 方法[2]来处理 C# 方法的返回值,以此来模拟一个异步等待返回值并处理的过程。
Javascript 端:
- 定义Function:
InvokeWebFunction(funcname, args)
- 此方法提供给 C# 端使用,用于 C# 端调用 Javascript 端的指定 function。
- 此方法同时用于在 javascript 调用 C# 方法后,等待、接收并处理 C# 方法的返回值。
- functionname: 要调用的 javascript 方法名
- args: 要调用的 javascript 方法的参数,json 格式的字符串。
- 定义Function:
InvokeServerFunction(classname, funcname, args, callback)
- 此方法提供给 Javascript 端使用,用于 Javascript 端调用 C# 端的指定 function。
- classname: 要调用的 C# 方法所在类的完全限定名。
- functionname: 要调用的 C# 方法名。
- args: 要调用的 C# 方法的参数,json-object。
- callback: 回调函数,接收并处理 C# 方法的返回值,返回值以 json-string 的格式返回。
代码如下:
/**
* protocol.js
*/
/**
*
* @param {string} funcname
* @param {json-string} args
*/
var InvokeWebFunction = function (funcname, args) {
window.tools.log(`function name:${funcname}`)
if (/^window.tempFunc/ig.test(funcname)) {
let source = funcname.match(/window.tempFunc\['([\S]*?)'\]/)
if (source[1]) {
let key = source[1]
try{
window.tools.log(`temp function key:${key}`)
window.tempFunc[key](args)
window.tempFunc.remove(key)
}
catch (err) {
alert(err)
}
}
return
}
try {
window[funcname](args)
}
catch (err) {
alert(err)
}
}
/**
*
* @param {string} funcname 需要调用的服务端方法名称
* @param {object} args 调用服务端方法时所需的参数
* @param {function} callback 用于接收服务端方法返回值的回调方法(回调方法需要接受json-string类型的参数)
*/
var InvokeServerFunction = function (classname, funcname, args, callback) {
let callbackname = null
if (callback) {
// 如果存在callback,则先将其置于 window.tempFunc 中并返回唯一函数名称义工后端调用
callbackname = window.tempFunc.append(callback)
}
// !!注意这里:这就是 Step 6 中需要添加事件监听的原因
let event = new MessageEvent('InvokeServerFunction', {
view: window,
bubbles: false,
cancelable: false,
data: JSON.stringify({
classname: classname,
funcname: funcname,
callback: callbackname,
args: JSON.stringify(args)
})
})
document.dispatchEvent(event)
}
window.tools = {
generateKey: function () {
return 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8)
return v.toString(16)
})
},
getQueryString (key) {
let source = window.location.href
let index = source.indexOf('?')
let queryString = ''
if (index > 0) {
queryString = source.substr(index + 1)
}
if (key) {
let regex = new RegExp(`(^|&)${key}=([^&]*)(&|$)`)
if (!queryString) {
return undefined
}
let result = queryString.match(regex)
if (result) {
return unescape(result[2])
}
return undefined
}
return queryString
},
log: function (msg) {
let trace = window.tools.getQueryString('trace')
if (trace) {
alert('trace:\r\n' + msg)
}
console.log(msg)
}
}
window.tempFunc = {
append: function (func) {
let key = `${func.name}.${window.tools.generateKey()}`
window.tempFunc[key] = func
return `window.tempFunc['${key}']`
},
remove: function (key) {
if (key === 'append' || key === 'remove') {
return
}
if (window.tempFunc[key]) {
delete window.tempFunc[key]
}
}
}
C# 端:
public void InvokeServerFunction(string args)
- 用于 接收 来自 Javascript 端的调用。
public void InvokeWebFunction(string funcname, object args)
- 用于 C# 端调用 Javascript 端的指定方法。
核心代码如下:
public class ProtocolHandler
{
private GeckoWebBrowser _browser;
public ProtocolHandler(GeckoWebBrowser browser)
{
_browser = browser;
}
public void InvokeServerFunction(string args)
{
var argsObj = JsonConvert.DeserializeObject<ServerFunctionInvokeArgs>(args);
var type = Type.GetType(argsObj.ClassName);
var instance = type.Assembly.CreateInstance(argsObj.ClassName);
var method = type.GetMethods().FirstOrDefault(item => item.Name == argsObj.FuncName);
object result = null;
if (method != null)
result = method.Invoke(instance, new object[1] { argsObj.Arguments });
if (string.IsNullOrEmpty(argsObj.Callback))
return;
var script = _browser.Document.CreateHtmlElement("script");
var parameters = JsonConvert.SerializeObject(result);
var content = string.Empty;
content += "try {";
content += $" let parameters = {parameters};";
content += $" InvokeWebFunction(\"{argsObj.Callback}\", parameters);";
content += "}";
content += "catch(err) {";
content += " alert(err);";
content += "}";
script.InnerHtml = content;
_browser.Document.Head.AppendChild(script);
}
public void InvokeWebFunction(string funcname, object args)
{
if (_browser.Window == null)
throw new Exception("broswer not ready.");
var parameters = JsonConvert.SerializeObject(args);
var script = $"InvokeWebFunction('{funcname}', '{parameters}')";
using (var context = new AutoJSContext(_browser.Window))
{
context.EvaluateScript($"InvokeWebFunction('{funcname}', '{parameters}')");
}
}
}
STEP 6. 添加事件监听
注意:事件监听代码必须放在 browser.Navigate()
方法之后,因为事件监听是针对每一个网页进行监听的,网页还没打开,自然也就无法监听。
因为在 Javascript端 发起调用的方式是通过 new 一个 MessageEvent
并 document.dispatchEvent(event)
来发起调用的,所以 C# 端也必须添加对应的事件监听 listener 才能接收到来自前端的调用。
private void Form1_Load(object sender, EventArgs e)
{
browser.Navigate("http://127.0.0.1/hello.html");
// 添加事件监听
browser.AddMessageEventListener("InvokeServerFunction", (string args) =>
{
_handler.InvokeServerFunction(args);
}
}
STEP 7. Javascript 端发起调用
这里就简单多了,直接使用上面 protocol.js
中的 InvokeServerFunction
即可发起调用。
document.getElementById('btnLoad').onclick = function () {
var classname = 'GeckoWebDemo.Form1'
var funcname = 'GetDemoData'
var parameters = {
name: document.getElementById('keyword').value
}
// 发起调用
InvokeServerFunction(classname, funcname, parameters, showData/*callback, 可不传*/)
}
var showData = function (data) {
// TODO: 接收 C# 方法的返回值,并处理业务逻辑
}
注意:请将 web 文件夹中的文件放于对应的web站点下,检查javascript引用,并还原nuget包之后再使用。