Wiki

Clone wiki

java-cef / 使用V49提供的JavaScript Binding进行前后台交互

作为二次开发的一个重要特征,就是让前端JS可以访问后端提供的独特的功能,而实现它的方式就是JavaScript Binding。

本文可能有一些表述不清楚的地方,大家多从java-cef示例代码中对应,如果有不明白的可以与我联系。


* 使用CefBrowser类的executeJavaScript方法

org.cef.browser.CefBrowser类的executeJavaScript函数可以让我们执行一段js代码,可以理解为eval。

方法原型为

#!java

public void executeJavaScript(String code, String url, int line);

从注释上看,code参数就是需要执行的代码,这段代码内容具有完全js代码功能,只要浏览器能正常解析执行,写什么都可以;url参数是当执行出错时可以找到源码或者解决方案的网址(如果给了这个参数的有效值),line是代码所在行号,个人觉得没什么用。总之,后两个参数一般没什么用,除非你需要在错误时回溯。

这个函数没什么值得多说的,具体使用方式大家自行研究。但是有一点值得注意:你在代码(code字符串)中使用到的window和其他变量是MAIN_FRAME(主文档,即最外面的html)的,不能隐式跨域(无法直接对网页内某个iframe进行操作),除非你写了从MainFrame获取特定iframe并操作的代码。

* 使用MessageRouter

1. 解释一下MessageRouter

cef中MessageRouter是一个很强大的功能,如果说第一种executeJavaScript是单向的话,MessageRouter就是完全进行双边交互的一种方式。

从命名上看,MessageRouter首先是一个Router(路由),交换的内容是Message(消息)。它在前端JavaScript和后端Java代码间建立起一个通道,首先由前端JS创建这个通道并注册回调,在后端完成计算后回调函数。因此操作是异步的,前端的回调并不是马上就被执行,甚至不保证会被执行

我们分前端和后端来介绍一下MessageRouter。

从前端而言,一个MessageRouter对应一组(两个)window的函数,一个为启用路由通道,一个为取消路由通道,例如cefQuery和cefQueryCancel

#!javascript

// 启用路由通道
var request_id = window.cefQuery({
    request: 'my_request',
    persistent: false,
    onSuccess: function(response) {},
    onFailure: function(error_code, error_message) {}
});

// 取消路由通道(在被回调之前操作有效;在指定耗时之后调用这个函数表示已超时)
window.cefQueryCancel(request_id);

每次调用cefQuery都是一次通道的建立,同一个路由可以有多个目标对其建立通道(也就是可以多次调用cefQuery,但onSuccess等回调的执行时机完全取决于后端,是逐个还是批量,是立即返回还是延迟执行),前端js在创建通道时给定的参数中,request是事先与后端勾兑好的一个值,onSuccess函数和onFailure函数分别在运算成功和失败时被调用,都是异步的。

总之,对于前端而言,只需要事先与后端勾兑好路由名称(是window.cefQuery还是window.myQuery,这个由后端注册)和操作名称(request参数的值,后端拿到进行判断),然后在onSuccess和onFailure回调中处理业务逻辑(当然,如果有超时逻辑的话可以使用cancel关闭通道,节省后端运算开销)。

另外,persistent参数无luan用……它是否代表什么完全看后端心情,cef完全不在乎它。

从后端而言,处理通道建立事件的函数是org.cef.handler.CefMessageRouterHandlerAdapter类的onQuery函数(cancel函数我不另做介绍了)

#!java

public boolean onQuery(CefBrowser browser,
                     long query_id,
                     String request,
                     boolean persistent,
                     CefQueryCallback callback) {
if (request.indexOf("my_request") == 0) {
  callback.success("my_response");
  return true;
}
return false;
}

需要说明的是,这个函数只是处理建立事件,你不一定马上调用callback.success等回调,你可以使用变量保存callback,并另起线程进行运算,当得出需要的结果时,再调用callback的回调。

这个函数的返回值只是表示当前处理器是否允许通道建立,如果返回false,则cef继续查找下一个处理器(通过路由名称),如果所有处理器都不允许建立,则cef会自动调用callback的failure回调(errorcode为-1),路由也可以是链式的(同一个路由名称可以被多次注册,比如我们同一个名为cefQuery的路由有多个处理器,但它们通过request来区分是否是自己感兴趣的通道),如果返回true,则不往下查找。

这里有一个很有意思的东西:callback是一次性的,当后端第一次调用callback的success或failure之后,callback就失效了,所以大家在写代码时一定要注意,是否多线程中抢占了callback?是否处理器中返回false之前却把callback给占用了?etc.

2. 路由创建(后端做的事儿)

首先创建CefMessageRouter对象

#!java

CefMessageRouter msgRouter = CefMessageRouter.create();

create函数在没有给定参数时,默认以cefQuery/cefQueryCancel作为通道建立和关闭的函数名。可以显示指定函数名称。

#!java

CefMessageRouterConfig config = new CefMessageRouterConfig();
config.jsQueryFunction = "myQuery";
config.jsCancelFunction = "myQueryCancel";

CefMessageRouter msgRouter = CefMessageRouter.create(config);

那么,在JS端就要用window.myQuery了。

然后为路由添加处理器,处理器是org.cef.handler.CefMessageRouterHandlerAdapter的实现类,上文书说到了。

#!java

msgRouter.addHandler(new MessageRouterHandler(), true);
msgRouter.addHandler(new MessageRouterHandlerEx(client_), false);

addHandler函数第二个参数是是否添加到链式结构首位,也就是优先接受通道建立请求。

代码来自java-cef示例代码,同一个CefMessageRouter对象添加的处理器(handler)就是链式的,示例代码也是使用request参数进行区分的。

最后把路由注册到CefClient

#!java

client_.addMessageRouter(msgRouter);

以上代码出现在tests.detailed.MainFrame类137~140行。

3. 一些废话

示例代码中给我们提供了一些消息路由的使用场景和应用思路。

主要集中在消息的订阅和事件的分发。

例如前端注册一个通道,后端回调时间不定,可能为某种特定事件的发生,这种事件甚至可以是其他通道的建立!当事件发生时,再一次性调用所有已注册的通道回调函数。

* java-cef阉割的另一种JS交互方式

熟悉cef的朋友知道,google把JavaScript做了一些JIT操作,这个东西被成为V8引擎。

而cef(c/c++API版本)是有通过V8引擎直接暴露接口与前后端交互的,但java-cef并没有对此进行实现。

例如cef中有一个函数(c++语言描述)

#!c++

bool CefRegisterExtension(const CefString& extension_name,
                          const CefString& javascript_code,
                          CefRefPtr<CefV8Handler> handler);

它可以把一段javascript代码注册成为一个扩展,这个定制性应该比executeJavaScript更强,但java-cef没实现我们也没办法。

Updated