跨桌面端之組件化實踐
背景介紹
windows千牛功能很豐富,mac千牛什么時候可以把能力對齊?
相信所有跨平臺應用,都有遇到過這樣的窘境。由于平臺差異的復雜性,維護多端產(chǎn)品成本非常高,且常常存在多端體驗不一致的問題。情況就是這樣,而我們團隊維護了pc千牛和pc旺旺2款跨端產(chǎn)品,在效能和體驗的雙重壓力之下,搭建一個多端統(tǒng)一的pc應用跨平臺開發(fā)框架勢在必行。
本文主要介紹了千牛PC跨端框架中,我們關于組件化部分的思考、方案選擇、遇到的一些問題和解法。
所謂框架,它既是一個“框子”,有一定的約束性,也是一個“架子”,有一定的支撐性。IT語境中的框架,特指為解決一個開放性問題而設計的具有一定約束性的支撐結構。在此結構上可以根據(jù)具體 問題擴展、安插更多的組成部分,從而更迅速和方便地構建完整的解決問題的方案。
為什么要做組件化?
跨端框架為什么選擇做組件化?
框架本身一般不能直接解決某個具體問題,但為解決問題的相關組件提供了一些銜接、組合的基礎能力。
框架的科學性、易用性,直接決定了研發(fā)效率和產(chǎn)品質(zhì)量。
組件化是用來解決框架功能擴展、復用的一種非常合適的技術方案。
而用組件化模式設計的一個應用框架,一般具備以下特性:
- 極好的擴展性
- 極好的復用性
- 靈活度高,可以很方便組裝或下線功能
- 修改功能,影響范圍很小
- 非常適合團隊分工協(xié)作
這些優(yōu)點,每一項都是我們夢寐以求的,所以說組件化對我們幾乎是必然選擇。
組件化是什么?
組件化是指解耦復雜系統(tǒng)時將多個功能模塊拆分、重組的過程,有多種屬性、狀態(tài)反映其內(nèi)部特性。
舉個例子,你要造一個汽車,但是發(fā)現(xiàn)汽車實在太復雜了,很難實現(xiàn),于是:
- 你把汽車拆分成了底盤,發(fā)動機,變速箱,輪子等模塊,定義好了他們各自的職責。
- 然后你找來小伙伴幫忙,先約定好了組件標準,再讓每個人實現(xiàn)其中一個獨立模塊。
- 你還需要一個控制系統(tǒng),能夠讓這些模塊互相配合,協(xié)同工作。
- 由于大家都按照一個標準來開發(fā),這些模塊很容易被組裝到一起,進行管理和控制。
- 這樣,你們實現(xiàn)了一個汽車。
這里的一個功能模塊,就是一個組件,用來控制組件協(xié)作運行的系統(tǒng),就是組件框架。
組件框架需要解決的幾個問題:
- 如何發(fā)現(xiàn)組件
- 如何管理組件的生命周期
- 如何組件間調(diào)用
- 提供的公共基礎能力
怎么選擇組件化方案?
組件化的落地方案很多,我們怎么選擇適合自己的技術方案?
業(yè)界的組件化方案很多,例如windows下的com組件,andriod下的ARouter組件,基于消息總線的ths組件,千牛自研的prg::com組件,還有一些基于rpc框架,更寬泛意義上組件化(微服務)。
在我看來,組件化方案沒有最好的,只有相對合適的。根據(jù)業(yè)務場景,選擇一個滿足當前業(yè)務需要,又能適當照顧到未來發(fā)展需要,好用好維護的方案就可以。
這里提供一些組件化方案選型一些可參考的維度:
- 發(fā)現(xiàn)機制
- 通信機制
- 跨平臺
- 跨編程語言
- 維護成本
- 研發(fā)效率
- 編譯依賴
- 性能
- 穩(wěn)定性
在跨端千牛的場景下,我們的訴求優(yōu)先級是:
- 首先必須是支持跨平臺的,
- 其次是良好的可維護性,長期來看,可維護性對產(chǎn)品質(zhì)量、效能和研發(fā)體驗都影響深遠。
- 然后是良好的性能和穩(wěn)定性,
- 最后是較好的研發(fā)效能和研發(fā)體驗。
這里我們主要對比了ths組件和prg::com組件方案:
- ths組件:ths方案類似于一個rpc調(diào)用框架,所有調(diào)用以消息的形式在總線上傳遞,其運行時隔離&有中心節(jié)點切面,但其接口可維護性較差,無法在編譯期發(fā)現(xiàn)問題。ths方案更適合跨團隊場景,或開放場景。
- prg::com組件:prg::com組件類似于微軟的com組件,但它支持了跨平臺,并對com接口調(diào)用方式進行了優(yōu)化,調(diào)用方便。其接口的可維護性較佳,編譯時就可以發(fā)現(xiàn)接口兼容性問題,性能也非常不錯,十分適用于團隊內(nèi)部的組件化場景。
最終我們選用了自研的prg::com作為跨端框架組件化的技術方案,下面具體介紹一下這個方案。
跨端組件化實踐
組件化方案,包含框架能力和組件約束兩部分。
框架設計是否科學,組件約束下是否易開發(fā)、易使用、易維護,是組件化方案要考量的核心因素。
- 組件框架,提供了組件運行的基礎能力,主要包括:組件發(fā)現(xiàn)機制組件生命管理組件間通信其他公共基礎能力
- 組件約束,定義了組件開發(fā)時需要遵循的標準,其主要目的包括了:支持組件在prg框架上運行,例如組件都繼承自prg::com對象,并需要完成I接口注冊。支持組件跨平臺,例如ui組件需要遵守mvp分層,在替換ui渲染層時,能確保做到業(yè)務邏輯多端一致。為了便于團隊協(xié)作,例如文件結構、代碼分層、命名規(guī)則等,使用相同范式去開發(fā)和使用組件。……
組件約束是根據(jù)組件的類型、具體使用場景等因素,分別進行定義的,不同組件的標準并不完全相同。
例如ui組件和非ui組件的標準就有很大的不同。
后面介紹下prg框架,及我們在各種場景下定義的組件約束條件。
? prg框架
- 組件發(fā)現(xiàn)機制
prg框架利用模板技術,通過打包時掃描dll生成配置、加載dll時靜態(tài)注冊組件,實現(xiàn)了一套組件發(fā)現(xiàn)機制。
prg框架的組件發(fā)現(xiàn)機制,依賴于id注冊,組件對外只暴露類id和I接口,實現(xiàn)了組件間完全去依賴。
先來看一下示意代碼:
// 定義一個prg::com組件
class IxxxService;
DEFINE_IID(IxxxService, "{4E6A382D-1FDA-49C6-8521-E284DA7B71CC}")
DEFINE_CLSID(xxxService, "{D1A52645-7587-4885-ABFD-323BA62905F5}")
// 創(chuàng)建這個prg::com組件
scoped_refptr<IxxxService> spInterface;
prg::PrgCOMCreateInstance(c_uuidof(xxxService), spInterface);
//////////////////////////////////////////////////////////////////////////
// implement(不對外暴露)
class CxxxService
: public prg::CPrgCOMRootObject<prg::CCOMThreadSafeRefPolicy>
, public IxxxService
{
public:
DECLARE_PRGCOM_RUNTIME(CxxxService, c_uuidof(xxxService), "xxxService", "xxxService", prg::GetDependsCLSID())
BEGIN_PRGCOM_MAP(CxxxService)
PRGCOM_INTERFACE_ENTRY(IxxxService)
END_PRGCOM_MAP()
};
IMPLEMENT_PRGCOM_RUNTIME(CxxxService);
它的實現(xiàn)原理是:
- 打包時,掃描目錄下所有dll,遍歷調(diào)用 GetPrgCOMFactory接口,生成組件配置xml。
- 創(chuàng)建對象時,通過xml配置,找到并load對應的dll。
- 加載dll時,會創(chuàng)建prg::CPrgCOMObjectRuntime<T>靜態(tài)變量g_prgRuntime,并在構造時向PrgCOMFactory注冊clsid到this的映射關系。
- 根據(jù)clsid,在PrgCOMFactory中找到對應的g_prgRuntime變量,調(diào)用CreateInstance靜態(tài)方法,由于g_prgRuntime變量是帶了T類型信息的,就可以創(chuàng)建出對應的T對象。
這里利用了c++模版技術和靜態(tài)注冊技術,巧妙地完成組件解耦,解決了依賴問題和跨模塊調(diào)用問題。
- 組件生命管理
prg組件支持無感跨模塊創(chuàng)建、使用、釋放對象,真正做到了一次開發(fā),到處使用,開箱即用。
prg組件使用scoped_refptr引用計數(shù)管理內(nèi)存,使用者不需要自行管理內(nèi)存。
prg框架支持跨dll/dylib創(chuàng)建、使用、釋放對象,對使用者來說dll/dylib是完全無感的,指定要創(chuàng)建的對象類型,接口類型,實例名稱,就可以直接開始使用這個接口了,非常絲滑。
class IxxxService : public prg::IPrgCOMRefCounted
{
public:
base::event<void()> onDataChanged;
public:
virtual bool GetData(const std::string& data) = 0;
}
// 創(chuàng)建prg::com組件新實例
scoped_refptr<IxxxService> spInterface;
prg::PrgCOMCreateInstance(c_uuidof(xxxService), spInterface);
// 獲取prg::com組件(沒有則create,prg框架內(nèi)部會保存一份引用)
scoped_refptr<IxxxService> spInterface;
prg::PrgCOMGetInstance(c_uuidof(xxxService), instanceName, spInterface);
// 判斷prg::com組件實例是否存在
prg::PrgCOMHasInstance(c_uuidof(xxxService), instanceName, bhave);
// 刪除prg::com組件實例
prg::PrgCOMDropInstance(c_uuidof(xxxService), instanceName);
我們一般會將組件獲取封裝成像下面這樣的接口,對于使用者來說,調(diào)接口就像調(diào)用自己的代碼一樣方便。
/// 組件頭文件
inline scoped_refptr<IxxxService> GetIxxxService()
{
scoped_refptr<IxxxService> spInterface;
prg::PrgCOMGetInstance(c_uuidof(UIAppGuideWidget), "", spInterface);
return spInterface;
}
/////////////////////////////////////////////////////////////////////////
// 其他組件直接調(diào)用接口
std::string data;
GetIxxxService()->GetData(data);
- 組件間通信
prg組件的接口調(diào)用和事件訂閱。
接口調(diào)用
prg組件接口調(diào)用與com組件類似,區(qū)別在于prg::com做了更好用的封裝,可以直接get到I接口對象進行使用。(當然還是支持使用QueryInterface,可以通過QueryInterface獲得不同類型的I接口)
class IxxxService : public prg::IPrgCOMRefCounted
{
public:
base::event<void()> onDataChanged;
public:
virtual bool GetData(const std::string& data) = 0;
}
// 獲取組件
scoped_refptr<IxxxService> spInterface;
prg::PrgCOMGetInstance(c_uuidof(xxxService), instanceName, spInterface);
// 調(diào)用組件方法
spInterface->GetData(callback);
事件訂閱
事件訂閱派發(fā),依賴base::event實現(xiàn),是典型的觀察者模式。當事件觸發(fā)時,按照注冊順序挨個調(diào)用觀察者的base::callback,可以非常容易的完成復雜流程串聯(lián)。這里的event是實例級別的,配合prg的賬號隔離能力,可以很好的解決多賬號業(yè)務的事件派發(fā)問題。但目前base::event暫不支持按優(yōu)先級注冊派發(fā)。
class IxxxService : public prg::IPrgCOMRefCounted
{
public:
base::event<void()> onDataChanged;
public:
virtual bool GetData(const std::string& data) = 0;
}
// 獲取組件
scoped_refptr<IxxxService> spInterface;
prg::PrgCOMGetInstance(c_uuidof(xxxService), instanceName, spInterface);
// 訂閱組件事件
CBaseEventHelper::RegisterEvent(spInterface->onDataChanged, callback);
// 取消訂閱組件事件
CBaseEventHelper::UnRegisterEvent(spInterface->onDataChanged);
? prg框架的組件約束
prg::com組件要遵循什么約束條件?
不同類型的組件,標準是不一樣的,要說組件標準,首先要對組件進行分類。
以阿里旺旺應用為例,跨端旺旺包含的組件,大致可以分成以下幾類:
框架層:
- ali系pc應用基礎組件
- 平臺相關基礎組件
框架和基礎組件,是阿里系pc應用基座,這些組件由prg框架內(nèi)置,從而實現(xiàn)快速搭建pc跨端應用的能力。
應用層:
- 旺旺業(yè)務-非UI組
- 旺旺業(yè)務-UI組件
應用層組件,主要用來實現(xiàn)業(yè)務功能,這部分組件經(jīng)常要進行擴展和修改,是我們要重點關注的。
應用層組件,根據(jù)其技術實現(xiàn),又可以分成ui相關和ui無關兩種,ui組件會相對更加復雜。
(ps:UI組件上采用pv分層,p層負責控制界面邏輯,使用純c++實現(xiàn),view層只負責繪制和操作輸入,這樣在最大程度復用代碼,提高效率的同時,保證業(yè)務雙端一致。我們的ui組件都遵從這個標準。我們選用了Qt作為跨端UI框架,我們發(fā)現(xiàn),Qt并不能做到UI功能完全跨端,考慮到后續(xù)替換UI框架或適配新平臺的可能性,我們把Qt的使用范圍收斂在UI渲染部分,即view層。)
prg組件通用標準
prg::com組件基本標準,所有的prg組件都遵守。
每個prg組件,都以…Service命名,以 I…Service接口的方式對外暴露,在C…Service里實現(xiàn)。
Service概念:Service即是prgcom組件,是客戶端內(nèi)的獨立業(yè)務單元,是對獨立業(yè)務能力的抽象。
接口:IxxxService(在biz/interface目錄,IxxxService.h文件)
實現(xiàn):CxxxService(在biz/xxx/service目錄)
獲取實例:GetxxxService()
使用方法:
- 所有prg組件對外提供服務的方式是統(tǒng)一的。
- 使用者可以通過GetxxxService()接口獲取到prg組件實例,然后通過IxxxService提供接口和事件使用組件。
- 組件內(nèi)部實現(xiàn)CxxxService不對外暴露。
非UI組件標準
不包含ui界面的組件,平臺差異影響較小,內(nèi)部按業(yè)務需要設計,遵守prg組件基本標準即可。
- UI組件標準
跨端ui組件的標準,主要包括了mvp分層,ui生命周期管理,以及各種場景下的多ui組合等。
ui組件依然遵從prg組件的通用標準,也支持prg組件的所有特性。
Service:是一個prgcom組件對象,外部使用ui組件時,直接操作service,就像使用非ui組件一樣。
UI:是界面整體,ui里包含presenter、view, 這里ui和view要區(qū)分清楚。
Presenter:是界面的邏輯對象,p層控制了所有業(yè)務邏輯,也控制view的輸入輸出。
View:是界面的渲染對象,只負責界面渲染和用戶操作輸入。
在prg框架下,A組件調(diào)用組件B的UI接口:
ui組件的復雜性:
- 不同平臺下的ui機制不相同,界面風格和操作習慣也不同,如何確保雙端業(yè)務邏輯一致?
- ui對象的生命周期一般由ui框架內(nèi)部管理,如何確保ui組件的生命周期管理不出問題?
- 如果一個組件里,包含多個ui怎么處理?多個ui之間并列關系怎么處理,嵌套關系又怎么處理?
- ui組件的場景太多,光標準定義就很復雜,如何在實際項目中落地實施?
mvp分層結構
為了更好的維護和復用ui組件,并滿足跨端訴求,我們的ui組件都采用mvp模式進行開發(fā)。
ui組件除了遵從prgcom的標準之外,還需遵守額外的約束:
- 每個UI界面內(nèi)部,分為p-v兩層,其中p層負責邏輯控制,v層負責輸入輸出。
- 每個UI界面,開放一個IxxxUI接口,IxxxUI代表UI界面整體,內(nèi)部只包含一個GetPresenter()方法。
- p層開放一個IxxxPresenter接口,外部調(diào)用IxxxPresenter提供的方法來操作這個UI界面。
- p層定義了v層需實現(xiàn)的輸入輸出接口IxxxUIDelegate,這個接口由v層代理實現(xiàn),只在p層可見。
- v層只負責實現(xiàn)渲染和輸入用戶操作。
這種設計的好處,主要在于:
- 所有的邏輯由p定義和控制,p層由c++實現(xiàn),可以實現(xiàn)跨多端,mac和windows統(tǒng)一。
- view被收斂在內(nèi)部,僅實現(xiàn)了輸入輸出接口,非常輕量。view相關的對象(如QT對象)不會擴散。
- 替換view簡單,只需重新實現(xiàn)UIDelegate接口。
- 可以通過實現(xiàn)UIDelegate接口的mock,在p層做單元測試。
生命周期管理
ui組件的生命周期比一般組件復雜,因為ui組件有一部分對象的生命周期是ui框架來管理的。
買旺跨端框架,ui選型是Qt框架,ui對象生命周期由Qt內(nèi)核管理,而組件生命周期是prg內(nèi)核管理。
當創(chuàng)建ui對象后,組件保存了ui對象的指針,以便和ui對象做業(yè)務交互,但ui對象的生命周期是qt內(nèi)部管理的。
因此,需要建立一種機制,當Qt銷毀ui對象的時候,需要通知到我們的組件這個ui對象已經(jīng)被銷毀了。
創(chuàng)建和調(diào)用流程:
銷毀流程:
各種UI界面的處理
單個UI界面
多個平級UI界面
父子UI界面
多層UI界面嵌套下的接口調(diào)用
由于嵌套層級太多,每一層都要開接口傳遞,會帶來大量工作量,因此提供泛化接口傳遞的解決方案。
可參考案例IAliwangwangChatBase.h的實現(xiàn),完成接口傳遞。
組件代碼自動生成?
跨平臺、標準化、低耦合,往往意味著編碼更加繁瑣,也意味著難以落地。
舉個例子,假如我在組件A要去操作組件B,顯示一個對話框,我需要寫的代碼有:
- IxxBService 組件接口
- CxxBService 組件實現(xiàn)
- IxxBPresenter P層接口
- CxxBPresenter p層實現(xiàn)
- IxxBPresenter::IxxBViewDelegate p層定義的ViewDelegate接口
- CxxBView view層實現(xiàn)
要在6個對象里寫代碼,這簡直就是一個災難!?。〉强紤]到長期的可維護性、跨平臺,又必須這么做。
于是,我們開發(fā)了一個代碼生成工具,根據(jù)上面各種情況下的ui關系,可以從模板自動生成組件代碼。
由于篇幅限制,這里不具體展開。
? 產(chǎn)生的效果
這套跨端組件化方案,已在跨端千牛/跨端旺旺產(chǎn)品中落地,目前雙產(chǎn)品三端已經(jīng)發(fā)布上線。
(目前win千牛功能 > 跨端千牛,跨端千牛的win版本尚未發(fā)布,敬請期待)
- 雙端完全一致的使用體驗,完全復用相同業(yè)務邏輯代碼。
- 雙端開發(fā)成本天然降低一半。
- 適合團隊協(xié)作,組件分拆,協(xié)同開發(fā)的效率高。
- 組件間完全解耦,可維護性大大增強。一次開發(fā),到處使用,簡單方便。
- 可用工具自動生成組件代碼,只需關注業(yè)務邏輯,效率高,風格一致。
- 集成了大量集團基礎能力,沉淀了pc跨端應用組件化框架,提供快速搭建阿里系pc應用能力。
再回到我們選擇這個方案時的目標,
- 首先是支持跨平臺,
- 其次是可維護性、擴展性,長期來看,可維護性對產(chǎn)品質(zhì)量、效能和研發(fā)體驗都影響深遠。
- 然后是良好的性能和穩(wěn)定性,
- 最后是較好的研發(fā)效能和研發(fā)體驗。
目前來看,prg組件框架在前3點上表現(xiàn)出色,在第4點研發(fā)體驗上,由于跨多端的嚴苛要求,ui組件分層較多,開發(fā)略顯繁瑣,我們通過自研組件代碼生成工具,緩解了這一問題。
總的來說,prg組件跨端框架可以在未來的3-5年里,很好的支撐起千牛/旺旺,甚至其他阿里系pc應用的業(yè)務。
跨端組件框架的演進思考
? 技術基礎能力完善
框架基礎能力,例如支持事件訂閱優(yōu)先級、支持組件鏈路/性能監(jiān)控、增加基礎能力組件等。
ui組件單元測試能力,ui組件都是pv結構的,ui邏輯都在p層,用簡單的uidelegate即可串聯(lián)ui邏輯,實現(xiàn)ui單元測試。
研發(fā)效能和研發(fā)體驗,完善代碼模板和自動化工具 ,實現(xiàn)接口級別代碼自動生成/補全,進一步提升研發(fā)效率和研發(fā)體驗。
? 業(yè)務上可能的嘗試
走向業(yè)務的組件化
組件化不光是技術概念,也是業(yè)務概念。復用帶來了低成本和一致性,解耦則帶來了業(yè)務的靈活性。
組件在技術上的靈活復用,能帶來的是業(yè)務上的靈活組合,快速嘗試。
例如旺旺系的IM能力,它可以是獨立的阿里旺旺產(chǎn)品,也可以集成到千牛里。組件就是積木,更多從業(yè)務角度去思考,提供更多更好用的積木,業(yè)務就能快速搭建出一個新大樓。
- 共享、共建pc組件庫
組件化不局限于團隊內(nèi),大家一起共享、共建的pc業(yè)務組件庫,才是更大程度發(fā)揮出組件化的價值。
希望pc業(yè)務越來越好!
團隊介紹
我們是大淘寶技術部行業(yè)與商家技術跨終端技術團隊,業(yè)務上負責為千萬級商家打造最高效的一站式工作臺千牛,為淘寶上億商家和消費者提供穩(wěn)定高效的端到端消息IM服務;技術上深耕C++跨終端及PC桌面端技術(Windows&Mac),為商家,消費者提供穩(wěn)定,可靠,高效的客戶端產(chǎn)品。