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 时才返回此参数。该参数可以设置为空