GeckoWebBrowser的使用及其前后端交互示例

momo314相同方式共享非商业用途署名转载

最近项目中遇到一个需求,需要在一个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 一个 MessageEventdocument.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包之后再使用。

✎﹏ 本文来自于 momo314和他们家的猫,文章原创,转载请注明作者并保留原文链接。