Javascript和BHO的相互调用简介
作者:magictong
Javascript调用BHO里面的函数
方案一、脚本扩展(window.external)
Window.external本身是浏览器提供的用来调用浏览器的外部方法的,譬如IE里面就提供了很多外部方法(参考[1])。如果我们想脚本能调用插件的函数,可以通过扩展这个接口的方法来实现(也就是脚本扩展)。
BHO实现要点
首先实现IDocHostUIHandler接口([3]),这个接口本身是IE暴露的一个界面替代接口(可以修改部分界面表现),通过ICustomDoc接口的SetUIHandler方法把IDocHostUIHandler设置到相应的页面上面,从而达到改变页面部分界面效果的作用,不过我们这里不用来改变界面,因此IDocHostUIHandler接口的大部分方法都可以简单实现,重点实现GetExternal方法即可。
GetExternal方法的实现也并不是很复杂,它只需要返回一个IDispatch接口即可,熟悉IE框架下面脚本和插件交互流程的童鞋应该很清楚返回这个接口的意义所在。因此这个地方我们需要实现一个继承自IDispatch接口的COM类,这里也是较为麻烦的一个地方。
实现说明
我们可以使用BHO类来实现IDocHostUIHandler接口即可,注意重点实现GetExternal方法,其实也很简单(注意:IDocHostUIHandler的大部分方法都可以返回一个S_OK即可)。
GetExternal的实现:
HRESULT STDMETHODCALLTYPECAdPromotionImp::GetExternal(IDispatch __RPC_FAR*__RPC_FAR *ppDispatch)
{
if(ppDispatch)
{
*ppDispatch = (IDispatch*)(new (std::nothrow) CJSObject());
}
return S_OK;
}
下面重点看一下这个CJSObject类的实现,它需要实现IDispatch接口,意味着你得把IUnknown接口也一并实现。IUnknown接口的实现就不详述了,看看IDispatch接口的四个方法的实现,GetTypeInfoCount和GetTypeInfo简单返回S_OK即可。另外两个方法的实现如下:
HRESULT STDMETHODCALLTYPECJSObject::GetIDsOfNames(
REFIID riid,
LPOLESTR __RPC_FAR*rgszNames,
UINT cNames,
LCID lcid,
DISPID __RPC_FAR*rgDispId)
{
if(!rgszNames || 0== cNames || NULL== rgDispId)
{
return S_OK;
}
*rgDispId = DISPID_UNKNOWN;
CComBSTR bstrRgszName(*rgszNames);
if (bstrRgszName== JS_CALL_FUN)
{
*rgDispId = DISP_ID_TESETSCRIPT;
return S_OK;
}
// 对于参数不正确,依然return S_OK 防止浏览器报脚本错误
return S_OK;
}
HRESULT STDMETHODCALLTYPECJSObject::Invoke(
DISPID dispIdMember,
REFIID riid,
LCID lcid,
WORD wFlags,
DISPPARAMS __RPC_FAR*pDispParams,
VARIANT __RPC_FAR*pVarResult,
EXCEPINFO __RPC_FAR*pExcepInfo,
UINT __RPC_FAR*puArgErr)
{
if(0 == (wFlags& DISPATCH_METHOD ))
{
// 对于参数错误的情况也return S_OK 防止浏览器报脚本错误
return S_OK;
}
if(dispIdMember ==DISP_ID_TESETSCRIPT)
{
::MessageBoxW(0, L"JS调用BHO函数TestScript()", L"TestScript", 0);
}
return S_OK;
}
要实现更复杂的函数就自己去想象吧……
另外,还得找一个地方去调用ICustomDoc接口的SetUIHandler方法哦!在哪里调用就看你的需求了而且要防止重入(在你感兴趣的网页需要且仅需调用一次)。
HRESULThr = E_FAIL;
CComPtr<IDispatch> spDoc;
CComQIPtr<IHTMLDocument2> spHTML;
m_spWebBrowser->get_Document(&spDoc);
spHTML= spDoc;
if(spHTML)
{
CComQIPtr<ICustomDoc, &IID_ICustomDoc>spCustomDoc;
hr = spHTML->QueryInterface(__uuidof(ICustomDoc), (void**)&spCustomDoc);
if (SUCCEEDED(hr) && spCustomDoc)
{
hr = spCustomDoc->SetUIHandler(this);
}
}
有副作用吗?
实现IDocHostUIHandler时要注意,为了避免副作用(因为你替换了系统原来的IDocHostUIHandler接口),在实现该接口的某些方法时为了避免内存泄漏,你应该把方法的调用总是转发给原来的IDocHostUIHandler接口,方法是在SetUIHandler之前先获取原来的IDocHostUIHandler接口并保存,然后在ShowUI和HideUI等方法里面转调源IDocHostUIHandler接口进行处理。
优缺点
在上面的讨论中零零碎碎有提到过,优点的话譬如实现相对来说比较简单等等,缺点就比较明显了副作用比较大,实现IDocHostUIHandler接口时要防止影响原始的功能(譬如另外一个插件也实现了这个接口),应用场景比较有限,另外安全性需要额外小心,因为函数名是固定的,需要保护好函数的参数等等。
测试
<html>
<head>
<script language='javascript'>
function call_external(){
try
{
window.external.TestScript();
}
catch(err)
{
alert(err.description);
}
}
</script>
</head>
<body onload="call_external();">
<center><div><span>HelloMagictong!!</span>
</div>
</center>
</body>
</html>
执行成功
方案二、导出类(属性注入)
上面的方法副作用很多,那么是否有其它的方法可以使用呢?答案是肯定的。还记得上面的方法里面在实现GetExternal时,返回了一个IDispatch接口给外部的脚本执行环境,对window.external空间进行了扩展的,导出类的方式其实也是类似的,但是扩展的是脚本的window空间,大概你可以这样认为,假如你导出了一个名字为KClass的对象给脚本的window空间,同时你的KClass对象实现了IDispatch接口,支持一个名为DoFunciton的函数,那么JS脚本里面就可以这样调用window.KClass.DoFunciton()。我们来看怎么实现。
在上面的方法中,我们已经把实现了IDispatch接口的类实现好了,就是CJSObject,可以直接拿来用,没有问题,关键看导出类的部分就可以了。
bool CAdPromotionImp::__ExtendJsWindow()
{
// 首先获取document2接口
CComPtr<IDispatch>spDispatch;
HRESULT hr = m_spWebBrowser->get_Document(&spDispatch);
if (!SUCCEEDED(hr) || !spDispatch)
return false;
CComQIPtr<IHTMLDocument2>pHTMLDoc(spDispatch);
if (!pHTMLDoc)
return false;
// 获取htmlwindow窗口
CComPtr<IHTMLWindow2>spWindow;
hr = pHTMLDoc->get_parentWindow(&spWindow);
if (!SUCCEEDED(hr) || !spWindow)
return false;
CComQIPtr<IDispatchEx>pHtmlWindow = spWindow;
if (!pHtmlWindow)
return false;
CComBSTR propName(L"KClass");
DISPID dispid= 0;
pHtmlWindow->GetDispID(propName, fdexNameEnsure,&dispid );
if (!SUCCEEDED(hr))
return false;
CComVariant varAdBho((IDispatch *)(new (std::nothrow) CJSObject()));
DISPPARAMS disParams= {&varAdBho, 0, 1, 0};
hr = pHtmlWindow->InvokeEx( dispid,LOCALE_USER_DEFAULT, DISPATCH_PROPERTYPUTREF, &disParams,NULL, NULL,NULL );
if (!SUCCEEDED(hr))
{
hr = pHtmlWindow->InvokeEx( dispid,LOCALE_USER_DEFAULT, DISPATCH_PROPERTYPUT, &disParams,NULL, NULL,NULL );
}
if (!SUCCEEDED(hr))
{
return false;
}
return true;
}
有副作用吗?
基本没有。
优缺点
其实实现起来比上面的方法不会复杂。而且没有副作用,不会干扰其它插件或者浏览器的固有行为。导出类可以做命名随机和加密,可以降低命名冲突的可能性,同时也可以降低其它网页对插件的恶意攻击行为。另外需要注意的是,为了加强代码的健壮性,防止内存泄漏,建议调试时严格控制下导出类的声明周期。
测试
<html>
<head>
<script language='javascript'>
function call_external()
{
try
{
window.KClass.TestScript2();
}
catch(err)
{
alert(err.description);
}
}
</script>
</head>
<body onload="call_external();">
<center><div><span>HelloMagictong!!</span>
</div>
</center>
</body>
</html>
执行成功
方案三、Axtivex控件
这个东东生来的理由就是为了让脚本来调用它,扩展IE的功能,但是因为不在本文的讨论范围,因此不再讨论。
BHO调用Javascript里面的函数
这个相对来说简单一些,主要就是通过调用IHTMLWindow2接口的execScript来完成对JS脚本里面函数的调用。
看这个方法说明,很直观吧。
直接看代码吧。
bool CAdPromotionImp::__CallJSFunction()
{
CComPtr<IDispatch>spDispatch;
HRESULT hr = m_spWebBrowser->get_Document(&spDispatch);
if (!SUCCEEDED(hr) || !spDispatch)
return false;
CComQIPtr<IHTMLDocument2>pHTMLDoc(spDispatch);
if (!pHTMLDoc)
return false;
CComPtr<IHTMLWindow2>spWindow;
hr = pHTMLDoc->get_parentWindow(&spWindow);
if (!SUCCEEDED(hr) || !spWindow)
return false;
VARIANT out;
hr = spWindow->execScript(CComBSTR("whocallme_Function()"), CComBSTR(L"javascript"), &out);
if (SUCCEEDED(hr))
return true;
return false;
}
调用时机
这个就看你的需求了,另外一个需要注意的时,你得等待JS代码(譬如你调用的是一个JS函数)已经被IE加载完成了,BHO再调用才能成功。
测试
<html>
<head>
<script language='javascript'>
function whocallme_Function()
{
alert("BHO call JS function!");
}
</script>
</head>
<body>
<center><div><span>HelloMagictong!!</span>
</div>
</center>
</body>
</html>
执行成功
参考资料
[1] external object http://msdn.microsoft.com/en-us/library/ms535246%28VS.85%29.aspx
[2] Popup WindowBlocker http://www.codeproject.com/Articles/4003/Popup-Window-Blocker
[3] IDocHostUIHandler interface http://msdn.microsoft.com/en-us/library/aa753260(v=vs.85).aspx
[4] JS调用BHO http://www.cnblogs.com/dlbrant/p/3142887.html
[5] 一点编写 BHO 的思路 http://bbs.csdn.net/topics/370215253