Cef4 -- 基于JS 扩展的消息传递

上篇代码贴的有点儿多,篇幅太长,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中转,做解析,然后再调用。

好了,就写到这里吧,写这篇感觉比较有成就感,因为都是自己踩过的坑,而不是导出去扒别人的内容。

猜你喜欢

转载自blog.csdn.net/moyebaobei1/article/details/81535996