cocos自带了webview组件,对于使用者来说,lua层接口非常简单:
local webView = ccexp.WebView:create()
webView:loadURL(url)
在2dx仓库中关于webview-win32的pr,但是好像并未完善webview js和c++的交互功能,仅仅实现了一个简单的webview展示。
关于win32 web browser的资料网上的晦涩难懂,找到的一个WebBrowser2-Demo。
C++接口的实现代码review
- 类的继承关系:DWebBrowserEvents2
class IDispatch{
virtual HRESULT invoke(...);//
}
class DWebBrowserEvents2:public IDispatch{
}
class Win32WebControl:public DWebBrowserEvents2{
}
- createWebView的实现
CAxWindow _winContainer; // 窗口对象的句柄
IWebBrowser2 *_webBrowser2;// webview控件
Win32WebControl::createWebView(){
HWND hwnd = cocos2d::Director::getInstance()->getOpenGLView()->getWin32Window();
_winContainer.Create(hwnd, NULL, NULL, WS_CHILD | WS_VISIBLE);
// 创建 ActiveX 控件,初始化它并在指定窗口中承载它。
auto hr = _winContainer.CreateControl(L"shell.Explorer.2");
// 查询指定的控件
hr = _winContainer.QueryControl(__uuidof(IWebBrowser2), (void **)&_webBrowser2);
}
- loadURL实现
void Win32WebControl::loadURL(BSTR url) const
{
VARIANT var;
VariantInit(&var);
var.vt = VT_BSTR;
var.bstrVal = url;
_webBrowser2->Navigate2(&var, NULL, NULL, NULL, NULL);
VariantClear(&var);
}
从c++的实现上观察到,核心在CAxWindow上,从网上找到的一些参考资料来看,win32平台的webview调用了ie的内核进行网页的加载渲染。
js调用c++
webview中的js代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<title>test</title>
</head>
<body>
<div id="btn">call cpp</div>
<script type="text/javascript" charset="UTF-8">
var btn = document.getElementById('btn')
btn.addEventListener('click', function() {
window.external.testFunction(10,'hello test!');
});
</script>
</body>
</html>
ie内核不支持js es6的特性,比如箭头函数
()=>{}等...
js调用c++是通过window.external实现的,而window.external是一个逐渐被废弃的标准,早期是用来实现js和外部程序进行交互的。
以上的网页在普通浏览器运行,点击call cpp是会报错的,提示没有testFunction函数。
在shell.Explorer.2的环境中,是可以正常的。
js call cpp 核心逻辑流程:
1.设置js层window.external.xxx调用到c++层的目标对象。这一步非常重要,是建立通讯的一个桥梁。所有js external的函数调用,都会派发到设置的实例。
// 参数只要是IDispatch的实例即可
_winContainer.SetExternalDispatch(this);
2.当js发生external调用时,会先回调步骤1设置的对象的GetIDsOfNames方法,在这一步,我们需要将调用的js函数名映射为一个id
至于为什么要将函数名映射为ID,猜测可能要和事件机制统一流程。
HRESULT STDMETHODCALLTYPE Win32WebControl::GetIDsOfNames(
REFIID riid,
LPOLESTR *rgszNames, // js external 调用的函数名
UINT cNames,
LCID lcid,
DISPID *rgDispId)
{
if(wcscmp(rgszNames[0], L"testFunction") == 0){
// 注意这里的rgDispId,将作为invoke的dispIdMember的入参
// 从指针参数可以大概推测出,就是希望开发者控制改写这个参数
*rgDispId = 199;
return S_OK;
}
return E_NOTIMPL; // 这个返回值将不会调用invoke
}
- 在这一步,才是我们真正要处理某个js调用真正逻辑的地方。也只有这里,我们才能拿到js传递的参数。
HRESULT STDMETHODCALLTYPE Win32WebControl::Invoke(
DISPID dispIdMember,
REFIID riid,
LCID lcid,
WORD wFlags,
DISPPARAMS *pDispParams,
VARIANT *pVarResult,
EXCEPINFO *pExcepInfo,
UINT *puArgErr)
{
switch(dispIdMember)
{
case 199:{
if (pDispParams->cArgs == 2)
{
VARIANTARG rgvarg0 = pDispParams->rgvarg[0];
VARIANTARG rgvarg1 = pDispParams->rgvarg[1];
// 参数是倒叙的,rgvarg包含一个union,需要根据type,检索正确的union类型
rgvarg0.bstrVal; // hello test
rgvarg1.intVal; // 10
// todo testFunction logic
return S_OK;
}
break;
}
}
}
从整体设计上思考,将js的每一个external函数调用都视为了事件,在invoke汇总分发处理,所以在做js函数名id映射时,是否需要注意id和原有的发生冲突?
比如:DISPID_NAVIGATECOMPLETE2、DISPID_COMMANDSTATECHANGE...
关于js函数名和dispId的映射,可以参考思路
IDispatch.invoke详细参数
- dispIdMember
标识成员。使用 GetIDsOfNames 或对象的文档来获取调度标识符。
在 ActiveX 客户端中,应使用 Invoke 来获取和设置属性值,或调用 ActiveX 对象的方法。dispIdMember 参数标识要调用的成员 - wFlag
| wFlag/value | 参数含义 |
|---|---|
| DISPATCH_METHOD/0x1 | 成员作为方法调用。如果属性具有相同的名称,则可以设置 this 和 DISPATCH_PROPERTYGET 标志。 |
| DISPATCH_PROPERTYGET/0x2 | 该成员作为属性或数据成员进行检索。 |
| DISPATCH_PROPERTYPUT/0x4 | 成员被更改为属性或数据成员。 |
| DISPATCH_PROPERTYPUTREF/0x8 | 成员通过引用分配而不是值分配进行更改。此标志仅在属性接受对对象的引用时才有效。 |
pDispParams
指向包含参数数组、命名参数的参数 DISPID 数组以及数组中元素数的 DISPIDAMS 结构的指针。pVarResult
指向要存储结果的位置的指针,如果调用者不期望结果,则为 NULL。如果指定了 DISPATCH_PROPERTYPUT 或 DISPATCH_PROPERTYPUTREF,则忽略此参数。
简单说:设置js external调用的返回值。pExcepInfo
指向包含异常信息的结构的指针。如果返回 DISP_E_EXCEPTION,则应填写此结构。可以为 NULL。puArgErr
rgvarg 中第一个有错误的参数的索引。参数以相反的顺序存储在 pDispParams->rgvarg 中,因此第一个参数是数组中索引最高的参数。仅当结果返回值为 DISP_E_TYPEMISMATCH 或 DISP_E_PARAMNOTFOUND 时才返回此参数。该参数可以设置为空