上篇代码贴的有点儿多,篇幅太长,Cef 中javascript 调用C++ 没有说完。这篇单独介绍通过javascript 扩展的方式调C++。
整个Render进程启动的时候会调用OnWebKitInitialized,用来初始化浏览器的相关操作,而且这个操作是针对整个render进程周期的,这个时候v8引擎刚初始化完成。
这里姑且认为Render进程维护一个v8 引擎,某一个时间只能存在一个上下文环境,但可以创建多个上下文,每次切换上下文环境需要先release上一个上下文环境,这个行为并非猜测,我在集成Cef的时候,当页面需要重新加载的时候,rander进程下OnContextCreated和OnContextReleased会成对调用。以上结论是Cef多进程模式下,断电跟踪的结论。
OnContextCreated/OnContextReleased是js binding 的基础,可以提供一个时机往当前上下文环境的window对象上添加自定义函数供js 调用,而v8 接口的execut方法可以截获对应的js调用,允许用户自定义一些操作。
OnWebKitInitialized 是全局js binding 的基础,可以在全局作用域扩展js、导出对象等。
本篇重点介绍如何使用OnWebKitInitialized,在不同的上下文环境中使用js 调用C++。
-
导出js 扩展供使用
render进程初始化(或者是v8殷勤初始化)的时候,根据自己需要定制对应js逻辑。我这里需要导出一个全局的自定义对象,对象上提供方法,方法上允许c++ 回调js(callback)。导出自定义对象【application】,提供reflect方法,两个参数,
name : json格式字符串,定义需要的参数,方便扩展(这里试着传入json对象,v8对象可以接受,但没找到方法提取参数,所以改传字符串);
callback : 传入一个函数对象,用来实现回调,name结构中包含一个key, 与改func对应;
void RenderApp::OnWebKitInitialized()
{
OutputDebugString(_T("ClientAppRenderer::OnWebKitInitialized, create js extensions\r\n"));
std::string app_code =
"var application;"
"if (!application)"
" application = {};"
"(function() {"
" application.GetId = function() {"
" native function GetId();"
" return GetId();"
" };"
//"})();";
// Registered Javascript Function, which will be called by Cpp
" application.reflect = function(name,callback) {"
" native function reflect();"
" return reflect(name,callback);"
" };"
"})();";
LOG(INFO) << "OnWebKitInitialized";
if (nullptr == m_v8Handler){
m_v8Handler = new V8HandlerImpl;
LOG(INFO) << "V8 handler is nullptr.";
}
CefRegisterExtension("v8/application", app_code, m_v8Handler);//第一个参数不能为空,否则报错,这个名字可以自定义
}
通过以上方法,js 环境就可以调用了,简单如下
application.reflect('{"type":1,"func":"idcard"}', function(res){
alert(res);
});
- render 截获js 请求,增加自定义操作
通过OnWebKitInitialized扩展的js方法,v8 execute方法中也会截获(行为同OnContextCreated binding的方法)
bool V8HandlerImpl::Execute(const CefString& name, /*JavaScript调用的C++方法名字*/
CefRefPtr<CefV8Value> object, /*JavaScript调用者对象*/
const CefV8ValueList& arguments, /*JavaScript传递的参数*/
CefRefPtr<CefV8Value>& retval, /*返回给JS的值设置给这个对象*/
CefString& exception)
{
if (name == "reflect")
{
if (arguments.size() == 2 && arguments[1]->IsFunction())
{
bool bObj = arguments[0]->IsObject();
bool bArr = arguments[0]->IsArray();
bool bBuf = arguments[0]->IsArrayBuffer();
//CefRefPtr<CefDictionaryValue> jsonObject(arguments[0]);
CefRefPtr<CefV8Value> json = arguments[0];
CefString v8Value = json->GetStringValue();
int type;
CefString funcNm;
CefRefPtr<CefValue> jsonObject = CefParseJSON(v8Value, JSON_PARSER_ALLOW_TRAILING_COMMAS);
if (jsonObject->IsValid())
{
CefRefPtr<CefDictionaryValue> dict = jsonObject->GetDictionary();
type = dict->GetInt("type");
funcNm = dict->GetString("func");
}
CefRefPtr<CefV8Value> callback_func_ = arguments[1];
CefRefPtr<CefV8Context> callback_context_ = CefV8Context::GetCurrentContext();
// 缓存
int browser_id = callback_context_->GetBrowser()->GetIdentifier();
callback_map_.insert(
std::make_pair(std::make_pair(funcNm, browser_id),
std::make_pair(callback_context_, arguments[1])));
// notify browser to deal msg
//TODO: doSomething() in native way
CefRefPtr<CefProcessMessage> msg = CefProcessMessage::Create(funcNm);
// Retrieve the argument list object.
CefRefPtr<CefListValue> args = msg->GetArgumentList();
// Populate the argument values.
args->SetSize(2);
args->SetInt(0, type);
args->SetString(1, funcNm);
CefV8Context::GetCurrentContext()->GetBrowser()->SendProcessMessage(PID_BROWSER, msg);
return true;
}
}
// Function does not exist.
return false;
}
这里需要说明的是,在v8 对象内维护一个map, 用来保存js 的上下文环境,js 交互因为是异步操作,所以需要缓存这些信息,等到自定义操作结束,还原上下文环境,执行js回调。缓存的map 结构如下
typedef std::map<std::pair<std::string, int>, std::pair<CefRefPtr<CefV8Context>, CefRefPtr<CefV8Value> > >CallbackMap;
CallbackMap callback_map_;
当前环境,整个render进程对应一个v8 handler, 对应一个callbackmap,因为浏览器会有上下文切换(最普通的页面跳转)render进程会先OnContextReleased释放当前上下文的信息(提供信息更新操作的入口),然后通过OnContextCreated重新构造上下文。所以OnContextReleased时我们需要清空之前上下文的信息,处理如下
void RenderApp::OnContextReleased(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefV8Context> context)
{
//m_v8Handler = nullptr;
if (nullptr != m_v8Handler)
{
m_v8Handler->clear();
}
}
好多blog说这时候要释放v8 handler,但是OnWebKitInitialized应该和v8 初始化是一一对应的,如果释放v8引擎,则全局的js 扩展就找不到了,js调用会失败,提示不存在的application和方法reflect。所以这里不能释放v8 handler,而应该清空map缓存信息,就是一般的map清空操作
void V8HandlerImpl::clear()
{
if (!callback_map_.empty()) {
CallbackMap::iterator it = callback_map_.begin();
for (; it != callback_map_.end();) {
if (it->second.first->IsSame(it->second.first))
callback_map_.erase(it++);
else
++it;
}
}
}
然后在下次js调用的时候继续缓存上下文环境,行为如前所述。
v8 截获自定义操作后,需要发消息通知browser,在browser进程中需要弹出对话框,做一些用户交互,然后获取数据再返回render进程。browser进程和render进程通信,在上一篇blog中已经讲过了,不再复述。这里从render收到browser进程返回数据说起。
bool RenderApp::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser, CefProcessId source_process, CefRefPtr<CefProcessMessage> message)
{
// extract message
CefRefPtr<CefListValue> args = message->GetArgumentList();
CefString strFunctionName = args->GetString(0);
CefString strArgs = args->GetString(1);
std::wstring Content(strArgs);
std::wstring strFunName(message->GetName());
if (nullptr == m_v8Handler)
{
LOG(ERROR) << "RenderApp::OnProcessMessageReceived V8 Handler is nullptr.";
//LOG(ERROR) << strFunName;
return false;
}
if (m_v8Handler->callback_map_.empty()) {
LOG(ERROR) << "CallbackMap is empty.";
//LOG(ERROR) << strFunName;
return false;
}
const CefString& message_name = message->GetName();
CallbackMap::const_iterator it = m_v8Handler->callback_map_.find(
std::make_pair(message_name.ToString(),
browser->GetIdentifier()));
if (it == m_v8Handler.get()->callback_map_.end()) {
LOG(ERROR) << "CallbackMap not find item.";
//LOG(ERROR) << strFunName;
return false;
}
// Keep a local reference to the objects. The callback may remove itself
// from the callback map.
CefRefPtr<CefV8Context> context = it->second.first;
CefRefPtr<CefV8Value> callback = it->second.second;
// Enter the context.
context->Enter();
CefV8ValueList arguments;
// First argument is the message name.
//arguments.push_back(CefV8Value::CreateString(message_name));
arguments.push_back(CefV8Value::CreateString(Content));
// Execute the callback.
CefRefPtr<CefV8Value> retval = callback->ExecuteFunction(NULL, arguments);
if (retval.get()) {
if (retval->IsBool())
bool handled = retval->GetBoolValue();
}
// Exit the context.
context->Exit();
return true;
}
前面做了map判空的处理,这是因为发现OnContextReleased中清空v8 ,callbackmap总是空的,后来发现切换上下文(页面跳转)v8 每次都被释放了,新的v8 handler, 找不到OnWebKitInitialized导出的js扩展了。
好了,一切都是那么美好,然后就是取出对应的上下文,然后调用enter/exit操作,进入/退出上下文,调用js。这里有个地方需要注意一下
CefV8ValueList arguments;
arguments.push_back(CefV8Value::CreateString(Content));// Execute the callback.
CefRefPtr<CefV8Value> retval = callback->ExecuteFunction(NULL, arguments);
if (retval.get()) {
if (retval->IsBool())
bool handled = retval->GetBoolValue();
}
如果js 自定义的callback方法有多个参数,则 所有参数要按顺序依次添加到arguments列表里,而不能加在ExecuteFunction(...)的参数列表。参数个数不匹配,也可能报错。这里也可以看出来,c++并不是直接调用的js , 需要经过v8中转,做解析,然后再调用。
好了,就写到这里吧,写这篇感觉比较有成就感,因为都是自己踩过的坑,而不是导出去扒别人的内容。