NPAPI插件開發(fā)詳細記錄:與JS交互
插件主要用于HTML頁面中增強HTML頁面可以支持的資源類型,,在HTML頁面中最重要的一個特性就是可以利用腳本語言來實現(xiàn)與用戶的交互,,之前的文章也提及過相關的議題,不過在交流過程中還是發(fā)現(xiàn)好多開發(fā)者對于這個主題有不太明白的地方,,這里再次詳細介紹一下在插件中如何與JS進行交互,。 以前的文章可以與本文互為參考:腳本化接口、插件接口腳本化,。
本文的例子代碼可以下載:無boost需安裝CMake,、 原來的scriptable demo
,。 插件對象可以在JS中使用document.getElementsByTagName或者document.getElementById來獲取頁面中已經(jīng)存在的插件對象,還可以在JS中使用document.createElement("object");來動態(tài)創(chuàng)建對象,,并為該對象設置type屬性,,接著將創(chuàng)建的這個對象添加到頁面中,這樣就動態(tài)創(chuàng)建了一個插件對象,。如下JS函數(shù)可以根據(jù)傳入的mimetype創(chuàng)建一個插件對象(chrome,、firefox測試有效,其他未測試):function newNPObject(mimetype) { var obj = document.createElement("object"); obj.type = mimetype; document.body.appendChild(obj); return obj; }那么瀏覽器是如何完成將插件轉換為JS能夠識別的對象的呢,?我們發(fā)現(xiàn),,在NPP_GetValue的實現(xiàn)中有: if (variable==NPPVpluginScriptableNPObject) { *(NPObject **)value = plugin->GetScriptableObject(); }也就是說,瀏覽器會調用NPP_GetValue (instance, NPPVpluginScriptableNPObject, value)并將來獲取插件的scriptable對象,。進一步看看plugin是如何獲取scriptable對象的: NPObject* CPlugin::GetScriptableObject() { if (!m_pScriptableObject) { m_pScriptableObject = NPN_CreateObject(m_pNPInstance, &CScriptObject::nsScriptObjectClass); } if (m_pScriptableObject) { NPN_RetainObject(m_pScriptableObject); } return m_pScriptableObject; }對象存在時用NPN_RetainObject來獲取對象,,對象不存在時用NPN_CreateObject來創(chuàng)建一個對象。 當我們在JS中設置/獲取屬性或者調用方法時,,都會在這個scriptable對象中操作,,在使用結束時(CPlugin的析構函數(shù)中)使用NPN_ReleaseObject(m_pScriptableObject);來釋放這個對象。 簡單解釋一下對象是如何創(chuàng)建的(一般情況下我們可以不知道,,只需要按照demo中的代碼使用就可以了,,如果只想知道如何實現(xiàn)與JS的交互請?zhí)料乱徊糠郑?,看看MDN上相關說明: NPObject *NPN_CreateObject(NPP npp, NPClass *aClass);
The function has the following parameters:
npp
The NPP indicating which plugin wants to instantiate the object.
aClass
The class to instantiate an object of.
第一個參數(shù)很好搞定,第二個參數(shù)比較費解,創(chuàng)建時傳入的&CScriptObject::nsScriptObjectClass實際上是基類nsScriptObjectBase的NPClass變量,,結合說明可以知道,,NPN_CreateObject是根據(jù)所傳入的NPClass類創(chuàng)建一個NPObject并返回這個對象的指針,。NPN_CreateObject中調用NPClass類的NPAllocateFunctionPtr成員來為NPObject分配內(nèi)存,,看到NPClass的NPAllocateFunctionPtr成員是nsScriptObjectBase::_Allocate函數(shù),該函數(shù)則是調用nsScriptObjectBase::AllocateScriptPluginObject來實現(xiàn)的,,AllocateScriptPluginObject的實現(xiàn)在Plugincpp中,,可以看到其實現(xiàn)代碼就是return (NPObject*)new CScriptObject(npp);也就是創(chuàng)建一個新的CScriptObject對象,這里繞過來繞過去這么復雜,,其實就是要做這樣一件事情:我們設計scriptableobject類時會新建一個類,,而基類nsScriptObjectBase卻需要用我們設計的scriptableobject類的構造函數(shù)來分配內(nèi)存并轉換成NPObject,最終由NPP_GetValue返回給瀏覽器,,JS實際上就是與瀏覽器獲取到的這個對象來交互的,。 仔細研究過npruntime代碼的人可能會發(fā)現(xiàn),npruntime中有一個很晦澀的宏DECLARE_NPOBJECT_CLASS_WITH_BASE及GET_NPOBJECT_CLASS,,當然從名稱可以知道是用基類聲明一個變量,,并用GET_NPOBJECT_CLASS來引用這個變量,這就相當于是我在基類中定義的nsScriptObjectClass,。 實現(xiàn)一個scriptable對象的類其實并不難,,只需要從NPObject派生一個類并逐一實現(xiàn)NPClass中的幾個函數(shù)指針所需要的函數(shù),。這里搞得如此復雜就是為了能夠設計一個基類,并一勞永逸的不再修改這個基類,。本章最后一個示例會給出實現(xiàn)一個最簡單的scriptable對象的例子,。 屬性在JS中一個對象具有的屬性可以比較靈活的設置,比如一個對象obj本來不具有屬性kit,,調用obj.kit會是undefined,,然而當我們設置obj.kit=some_val之后,,再次調用obj.kit就會有相應的屬性了,。另一方面,在實現(xiàn)插件dll的代碼中(后文稱為C++代碼中),,插件對象是一個派生自NPObject的對象,,我們也可以很方便的為其設置成員變量,要在插件中實現(xiàn)與JS交互,,那么就需要C++代碼中的屬性(變量)與JS中屬性能夠互相訪問,。 可以進行交互的屬性分為一般屬性及只讀屬性,只讀屬性是對于JS來說的,,畢竟插件中的代碼相對于JS來說是更加底層的,,可以不允許JS修改C++中保持的變量,但若想要防止C++更改JS中的變量值卻是比較不現(xiàn)實的,。 從NPObject的定義可以看到,,NPObject包括一個指向NPClass對象的指針和一個引用計數(shù)器。NPClass則由諸如hasProperty,、hasMethod等函數(shù)指針,。要實現(xiàn)一個可以與JS交互的插件,就需要實現(xiàn)hasProperty,、hasMethod等接口,。前文我們知道瀏覽器調用NPN_CreateObject創(chuàng)建scriptable對象,這里介紹我們在scriptable對象中實現(xiàn)可交互屬性,。 大概過程是這樣的:瀏覽器在獲取到scriptable對象之后,,就會調用對象的hasProperty、hasMethod來判斷該對象是否具有某個屬性或方法,,當JS中訪問屬性或調用函數(shù)是就會調用scriptable對象的getProperty,、involve等函數(shù)來獲取屬性值或者執(zhí)行函數(shù)。 要設置一個屬性(這里以foo為例),,首先需要定義一個NPIdentifier來方便保存屬性的標識,,一般的插件中是設置為全局static變量,我將其設置為CScriptObject類的static成員變量,,不設置為全局的,,如下: static NPIdentifier foo_id;
類的中聲明的static變量并不會初始化,,還需要在cpp文件中,對其初始化: NPIdentifier CScriptObject::foo_id;另外我設置一個變量來保存屬性值,,這個變量要CScriptObject類可以訪問,,或者通過某個函數(shù)訪問,簡便起見直接設置為CScriptObject類的私有成員變量: int m_vfoo;
接下來在適當?shù)奈恢脤@個id與要設置的屬性關聯(lián)起來,,我選擇在CPlugin類的構造函數(shù)中執(zhí)行: CScriptObject::foo_id = NPN_GetStringIdentifier("foo");如前所述,,瀏覽器會調用CScriptObject類的HasProperty來判斷是否具有某屬性,那么我們在HasProperty中如下實現(xiàn): bool CScriptObject::HasProperty(NPIdentifier name) { return name == foo_id; }在JS中可以設置屬性,,需要實現(xiàn)SetProperty bool CScriptObject::SetProperty(NPIdentifier name, const NPVariant *value) { if(name==foo_id){ if(value->type == NPVariantType_Int32) m_vfoo = NPVARIANT_TO_INT32(*value); return true; } }最后在GetProperty中返回屬性值: bool CScriptObject::GetProperty(NPIdentifier name, NPVariant *result) { if (name == foo_id) { INT32_TO_NPVARIANT(m_vfoo,*result); m_vfoo++; return true; } }為了與JS中設置屬性值進行區(qū)別,,每次獲取之后,把屬性的值+1可以在JS中多次獲取該屬性值,,發(fā)現(xiàn)每次獲取的值都會增加一個,,說明確實是獲取到了插件中設置的屬性。 在JS中設置/獲取foo屬性,,HTML中相應代碼為: property test: int property<input id = "fooinput" value="0"></input> <button onclick = "btnsetfoo();">set FOO</button> <button onclick = "btngetfoo();">get FOO</button><br />JS代碼(片段)如下: function btnsetfoo() { var val = document.getElementById("fooinput"); obj.foo = parseInt(val.value) ; } function btngetfoo() { alert(obj.foo); }如果想實現(xiàn)只讀屬性,,不實現(xiàn)SetProperty即可。 供JS調用的插件接口實現(xiàn)可以在JS中調用的接口,,過程與屬性相似,,這里以實現(xiàn)一個函數(shù)func為例,首先在CScriptObject類中聲明一個標識:static NPIdentifier func_id;
初始化: NPIdentifier CScriptObject::func_id;接下來將這個id與要設置的函數(shù)名關聯(lián)起來: CScriptObject::func_id = NPN_GetStringIdentifier("func");瀏覽器會調用CScriptObject類的HasMethod來判斷是否具有某個函數(shù),,那么我們在HasMethod中如下實現(xiàn): bool CScriptObject::HasMethod(NPIdentifier name) { return name == func_id; }JS中調用obj.func()時,,會執(zhí)行到Invoke,其中我們利用messagebox彈出一個消息框,,實現(xiàn)如下: bool CScriptObject::Invoke(NPIdentifier name, const NPVariant *args, uint32_t argCount, NPVariant *result) { if (name == func_id) { MessageBox(NULL,_T("func"),_T(""),0); return true; } return false; }在JS中調用func函數(shù),,HTML中相應代碼為: <button onclick ="btnclick();" >FUNC</button>JS代碼(片段)如下: function btnclick()
{
obj.func();
}
供插件調用的JS函數(shù)(JS callback)可以將JS函數(shù)作為回調供插件調用,假設我們需要插件調用JS函數(shù)如下:function objJSfunc() { alert("JS function called!"); } 在JS中,,函數(shù)其實也是一個object,,那么如何將這個object傳遞給插件,并在插件中執(zhí)行呢,?我們可以將這個object作為插件的一個屬性,,在執(zhí)行的時候利用NPN_InvokeDefault來執(zhí)行,以下是完整過程:
首先需要一個變量來保存這個JS函數(shù)或者函數(shù)的標識,,JS函數(shù)作為一個對象,,因此可以將其保存為一個NPObject對象,可以用全局變量也可以將其作為某個類的成員變量,,我將這個NPObject作為CPlugin類的一個成員,,聲明成員 變量: NPObject* m_jsObj;在構造函數(shù)中初始化為NULL: m_jsObj = NULL;
使用完畢之后需要釋放這個對象,我們在析構函數(shù)中執(zhí)行: if (m_jsObj!=NULL) NPN_ReleaseObject(m_jsObj);接下來,將JS函數(shù)作為一個屬性,,與前文設置一般屬性是一樣的,,先聲明一個標識: static NPIdentifier jsfunc_id;
初始化: NPIdentifier CScriptObject::jsfunc_id;與要設置的屬性名稱關聯(lián)起來: CScriptObject::jsfunc_id = NPN_GetStringIdentifier("OnJsFunc");接下來在HasProperty中: bool CScriptObject::HasProperty(NPIdentifier name) { return name == jsfunc_id; }然后SetProperty: bool CScriptObject::SetProperty(NPIdentifier name, const NPVariant *value) { if (name == jsfunc_id) { CPlugin * plugin = (CPlugin*) m_npp->pdata; if (plugin->m_jsObj == NULL) { plugin->m_jsObj = NPN_RetainObject(NPVARIANT_TO_OBJECT(*value)); } return true; } }當然這個就不需要實現(xiàn)GetProperty了。這樣就實現(xiàn)了JS回調函數(shù)的設置,,只需要在JS中使用obj.OnJsFunc = objJSfunc;為其設置好需要調用的JS函數(shù)就可以了,。 設置好之后,就是在C++中如何調用這個JS函數(shù)了,,要調用這個函數(shù),,就需要訪問我們設置的m_jsObj,因此只要能夠訪問我們設置的這個變量的位置都可以執(zhí)行這個JS函數(shù),,之前我們設置了一個func供JS調用,,這里我們假設func執(zhí)行完畢之后需要調用我們設置的這個JS函數(shù),可以在func的執(zhí)行代碼最后加上調用JS函數(shù)的代碼,,Invoke函數(shù)就變?yōu)槿缦滦问搅耍? bool CScriptObject::Invoke(NPIdentifier name, const NPVariant *args, uint32_t argCount, NPVariant *result) { if (name == func_id) { MessageBox(NULL,_T("func"),_T(""),0); CPlugin* plugin = (CPlugin*) m_npp->pdata; if (!(!plugin->m_jsObj)) { NPVariant result; NPN_InvokeDefault(m_npp,plugin->m_jsObj,NULL,0,&result); NPN_ReleaseVariantValue(&result); } return true; } return false; }以上就是設置JS回調的完整過程,,與JS交互有關的話題可能還包括編碼的轉換,,在遇到中文時處理不好可能會導致亂碼,,只要記住一個原則就是JS處理的字符都是UTF-8編碼的,而C++中的字符可能是unicode的也可能是ansi的,,因此需要根據(jù)實際情況進行編碼的轉換就可以解決中文亂碼的問題,。我給出的scriptdemo還包含:str屬性可以設置字符串類型的屬性,funci2i處理輸入為int輸出為int的函數(shù),,funcs2s處理輸入為字符串輸出為字符串的函數(shù),。目前沒有發(fā)現(xiàn)有亂碼的問題,因此這里就不再對編碼轉換的話題做過多的介紹了,,如果有朋友發(fā)現(xiàn)scriptdemo中有亂碼的問題,,請及時反饋給我,需要的話以后再來補充,。 接下來實現(xiàn)一個簡單的JS數(shù)組對象,,可以說是一個簡化的scriptable對象的設計。 簡單的JS數(shù)組對象這里設計的一個JS數(shù)組對象是比較簡單的,,因為我在C++中將數(shù)據(jù)保存好,,然后以數(shù)組對象的方式返回到JS中,JS中只需要調用Size方法獲取數(shù)組中元素的個數(shù),,并使用At方法獲取數(shù)組中的某一個元素,。當然網(wǎng)上可能有更完善的JS數(shù)組的實現(xiàn),或者你可以在這個代碼的基礎上完善更多的功能,,在此僅貼出代碼,。頭文件: class JsArrayObject : public NPObject { private: vector<std::wstring> array_; static NPIdentifier method_At; static NPIdentifier method_Size; public: static NPClass nsJsObject; JsArrayObject(); ~JsArrayObject(){} public: std::wstring At(size_t idx); size_t Size(); void Clear(); bool Empty(); void PushBack(std::wstring val); static NPObject *_Allocate(NPP npp, NPClass *aClass); static void _Deallocate(NPObject *npobj); static void _Invalidate(NPObject *npobj); static bool _HasMethod(NPObject *npobj, NPIdentifier name); static bool _Invoke(NPObject *npobj, NPIdentifier name, const NPVariant *args, uint32_t argCount, NPVariant *result); static bool _InvokeDefault(NPObject *npobj, const NPVariant *args, uint32_t argCount, NPVariant *result); static bool _HasProperty(NPObject *npobj, NPIdentifier name); static bool _GetProperty(NPObject *npobj, NPIdentifier name, NPVariant *result); static bool _SetProperty(NPObject *npobj, NPIdentifier name, const NPVariant *value); static bool _RemoveProperty(NPObject *npobj, NPIdentifier name); static bool _Enumerate(NPObject *npobj, NPIdentifier **value, uint32_t *count); static bool _Construct(NPObject *npobj, const NPVariant *args, uint32_t argCount, NPVariant *result); };實現(xiàn)文件: NPIdentifier JsArrayObject::method_At; NPIdentifier JsArrayObject::method_Size; JsArrayObject::JsArrayObject() { method_At = NPN_GetStringIdentifier("At"); method_Size = NPN_GetStringIdentifier("Size"); } NPObject* JsArrayObject::_Allocate(NPP npp, NPClass *aClass) { return (NPObject*)new JsArrayObject(); } void JsArrayObject::_Deallocate(NPObject *npobj) { delete (JsArrayObject*)npobj; } void JsArrayObject::_Invalidate(NPObject *npobj) { } bool JsArrayObject::_HasMethod(NPObject *npobj, NPIdentifier name) { return name == method_At || name ==method_Size ; } bool JsArrayObject::_Invoke(NPObject *npobj, NPIdentifier name, const NPVariant *args, uint32_t argCount, NPVariant *result) { if (name == method_At) { if(argCount < 1) return false; int val = args[0].value.intValue; std::wstring str = ((JsArrayObject*)npobj)->At(val); std::string pstr = ult::UnicodeToUtf8(str); char* npOutString = (char*) NPN_MemAlloc(pstr.length() + 1); if (!npOutString) return false; strcpy(npOutString, pstr.c_str()); STRINGZ_TO_NPVARIANT(npOutString,*result); return true; } if (name == method_Size) { int val = ((JsArrayObject*)npobj)->Size(); INT32_TO_NPVARIANT(val, *result); return true; } return false; } bool JsArrayObject::_InvokeDefault(NPObject *npobj, const NPVariant *args, uint32_t argCount, NPVariant *result) { return false; } bool JsArrayObject::_HasProperty(NPObject *npobj, NPIdentifier name) { return false; } bool JsArrayObject::_GetProperty(NPObject *npobj, NPIdentifier name, NPVariant *result) { return false; } bool JsArrayObject::_SetProperty(NPObject *npobj, NPIdentifier name, const NPVariant *value) { return false; } bool JsArrayObject::_RemoveProperty(NPObject *npobj, NPIdentifier name) { return false; } bool JsArrayObject::_Enumerate(NPObject *npobj, NPIdentifier **value, uint32_t *count) { return false; } bool JsArrayObject::_Construct(NPObject *npobj, const NPVariant *args, uint32_t argCount, NPVariant *result) { return false; } NPClass JsArrayObject::nsJsObject = { NP_CLASS_STRUCT_VERSION_CTOR, JsArrayObject::_Allocate, JsArrayObject::_Deallocate, JsArrayObject::_Invalidate, JsArrayObject::_HasMethod, JsArrayObject::_Invoke, JsArrayObject::_InvokeDefault, JsArrayObject::_HasProperty, JsArrayObject::_GetProperty, JsArrayObject::_SetProperty, JsArrayObject::_RemoveProperty, JsArrayObject::_Enumerate, JsArrayObject::_Construct }; std::wstring JsArrayObject::At(size_t idx) { return array_.at(idx); } |
|