当前位置: 首页 > 科技观察

跨桌面组件实践

时间:2023-03-21 18:00:11 科技观察

背景介绍Windows千牛的功能非常多,mac千牛什么时候才能齐心协力呢?相信所有的跨平台应用都遇到过这样的困境。由于平台差异的复杂性,多端产品的维护成本非常高,经常会出现多端体验不一致的问题。是这样的,我们团队维护了两个跨端产品,pc千牛和pc旺旺。在性能和体验的双重压力下,构建多端统一的pc应用跨平台开发框架势在必行。本文主要介绍了我们在千牛PC跨端框架中组件化的思考、方案选择、遇到的一些问题和解决方案。所谓架子,既是“架子”,有一定的约束力,又是“架子”,有一定的支撑力。IT上下文中的框架特指具有特定约束的支持结构,旨在解决开放性问题。基于这种结构,可以根据具体的问题扩展和插入更多的组件,从而更快速、更方便地构造出完整的问题解决方案。为什么要做组件化?跨端框架为什么选择组件化?框架本身一般不能直接解决某个具体问题,但它提供了一些基本能力,用于连接和组合解决问题的相关组件。框架的科学性和易用性直接决定了研发效率和产品质量。组件化是一种非常适合解决框架功能扩展和复用的技术方案。采用组件模型设计的应用框架一般具有以下特点:扩展性好、复用性好、灵活性高、易于组装或离线功能修改、影响范围小,非常适合团队分工协作是我们梦寐以求的,所以组件化几乎是我们必然的选择。什么是组件化?组件化是指在对一个复杂系统进行解耦时,将多个功能模块拆分重组的过程,各种属性和状态反映了其内部特性。比如你想造一辆汽车,但你发现这辆车太复杂了,实现不了,于是:你把汽车拆分成底盘、发动机、变速箱、车轮等模块,定义好各自的职责。然后你请朋友帮忙,先就组件标准达成一致,然后让大家实现其中一个独立的模块。您还需要一个控制系统,可以让这些模块协同工作,协同工作。由于大家都是按照一个标准来开发的,所以这些模块可以很容易的组装在一起进行管理和控制。这样,你就实现了一辆汽车。这里的一个功能模块就是一个组件,用来控制组件协同运行的系统就是组件框架。组件框架需要解决的几个问题:如何发现组件,如何管理组件的生命周期,如何调用组件间提供的公共基础能力,如何选择组件化方案?组件化的实现方案有很多,我们如何选择适合自己的技术方案呢?业界有很多组件化方案,比如windows下的com组件,andriod下的ARouter组件,基于消息总线的ths组件,千牛开发的prg::com组件,还有一些广义上基于rpc框架的组件(微服务)).在我看来,没有最好的组件方案,只有相对合适。根据业务场景,选择满足当前业务需求,能够妥善照顾未来发展需求,易用易维护的解决方案。以下是组件方案选择的一些参考维度:发现机制、通信机制、跨平台、跨编程语言、维护成本、研发效率、编译、性能、稳定性。支持跨平台,可维护性好。从长远来看,可维护性对产品质量、性能和研发体验有着深远的影响。然后是良好的性能和稳定性,最后是更好的研发效率和研发体验。这里我们主要对比一下ths组件和prg::com组件方案:ths组件:ths方案类似于一个rpc调用框架,所有的调用都以消息的形式在总线上传递,其运行时是隔离&有acentralnodeaspect,但其接口的可维护性较差,编译时无法发现问题。这个方案比较适合跨团队的场景或者开放的场景。prg::com组件:prg::com组件类似于微软的com组件,但支持跨平台,优化了com接口的调用方式,调用方便。它的接口可维护性比较好,编译的时候可以发现接口兼容性问题,性能也很好,非常适合团队内部的组件化场景。最终我们选择了自研的prg::com作为跨端框架组件化的技术方案。下面详细介绍该解决方案。跨端组件化实践组件化方案包括框架能力和组件约束两部分。框架设计是否科学,在组件的约束下是否易于开发、使用和维护,是组件化方案需要考虑的核心因素。组件框架提供了组件运行的基础能力,主要包括:组件发现机制、组件生命周期管理、组件间通信等通用基础能力。组件约束定义了组件开发过程中需要遵循的标准。其主要用途包括:运行在框架上,例如组件继承自prg::com对象,需要完成I接口注册。支持组件跨平台,比如ui组件需要遵守MVP分层,在更换ui渲染层时,可以保证业务逻辑跨多个终端一致。为了便于团队协作,如文件结构、代码分层、命名规则等,使用相同的范式来开发和使用组件。...组件约束是根据组件类型、具体使用场景等因素单独定义的,不同组件的标准也不完全相同。比如ui组件和非ui组件的标准就大不相同。prg框架和我们定义的各种场景下的组件约束,后面会介绍。?PRG框架组件发现机制prg框架通过模板技术实现了一套组件发现机制,通过打包时扫描dll生成配置,加载dll时静态注册组件。prg框架的组件发现机制依赖于id注册,组件对外只暴露classid和I接口,实现了组件间的完全去依赖。先来看看原理图代码://定义一个prg::com组件类IxxxService;DEFINE_IID(IxxxService,"{4E6A382D-1FDA-49C6-8521-E284DA7B71CC}")DEFINE_CLSID(xxxService,"{D1A52645-7587-4885-ABFD-323BA62905F5}")//创建这个prg::com组件scoped_refptrspInterface;prg::PrgCOMCreateInstance(c_uuidof(xxxService),spInterface);/////////////////////////////////////////////////////////////////////////实现(未公开)类CxxxService:publicprg::CPrgCOMRootObject,publicIxxxService{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);它的实现原理是:打包时,扫描目录下的所有dll,遍历调用GetPrgCOMFactory接口,生成组件配置xml。创建对象时,通过xml配置找到并加载对应的dll。dll加载时,会创建prg::CPrgCOMObjectRuntime静态变量g_prgRuntime,构造时会将clsid到this的映射关系注册到PrgCOMFactory。根据clsid,在PrgCOMFactory中找到对应的g_prgRuntime变量,调用CreateInstance静态方法。由于g_prgRuntime变量有T类型信息,所以可以创建对应的T对象。这里利用C++模板技术和静态注册技术,巧妙地完成了组件解耦,解决了依赖问题和跨模块调用问题。组件生命管理prg组件支持无感跨模块创建、使用、释放对象。prg组件使用scoped_refptr引用计数来管理内存,用户不需要自己管理内存。prg框架支持跨dll/dylib创建、使用和释放对象。对于用户来说,dll/dylib是完全不敏感的。只需指定要创建的对象类型、接口类型和实例名称,就可以直接开始使用这个接口。非常丝滑。classIxxxService:publicprg::IPrgCOMRefCounted{public:base::eventonDataChanged;public:virtualboolGetData(conststd::string&data)=0;}//创建一个新的prg::com实例componentscoped_refptrspInterface;prg::PrgCOMCreateInstance(c_uuidof(xxxService),spInterface);//获取prg::com组件(如果没有则创建,引用将保存在prg框架内)scoped_refptrspInterface;prg::PrgCOMGetInstance(c_uuidof(xxxService),instanceName,spInterface);//判断prg::com组件实例是否存在prg::PrgCOMHasInstance(c_uuidof(xxxService),instanceName,bhave);//删除prg::com组件实例prg::PrgCOMDropInstance(c_uuidof(xxxService),instanceName);我们一般会获取组件并将其封装成如下的接口。对于用户来说,调用接口就像调用自己的代码一样方便。///组件头文件内联scoped_refptrGetIxxxService(){scoped_refptrspInterface;prg::PrgCOMGetInstance(c_uuidof(UIAppGuideWidget),"",spInterface);返回spInterface;}/////////////////////////////////////////////////////////////////////////其他组件直接调用接口std::stringdata;GetIxxxService()->GetData(数据);组件间通信prg组件接口调用和事件订阅。接口调用prg组件的接口调用与com组件类似,不同的是prg::com做了更好的封装,可以直接拿到I接口对象使用。(当然仍然支持QueryInterface,可以通过QueryInterface获取不同类型的I接口)std::string&data)=0;}//获取组件scoped_refptrspInterface;prg::PrgCOMGetInstance(c_uuidof(xxxService),instanceName,spInterface);//调用组件方法spInterface->GetData(callback);eventsubscription事件订阅分发,依赖base::event实现是典型的观察者模式。当事件触发时,按照注册的顺序依次调用观察者的base::callback,可以轻松完成复杂的流程系列。这里的事件是实例级别的。配合prg的账户隔离能力,可以很好的解决多账户业务的事件分发问题。但是目前base::event不支持按优先级注册和分发。classIxxxService:publicprg::IPrgCOMRefCounted{public:base::eventonDataChanged;public:virtualboolGetData(conststd::string&data)=0;}//获取组件scoped_refptrspInterface;prg::PrgCOMGetInstance(c_uuidof(xxxService),instanceName,spInterface);//订阅组件事件CBaseEventHelper::RegisterEvent(spInterface->onDataChanged,callback);//取消订阅组件事件CBaseEventHelper::UnRegisterEvent(spInterface->onDataChanged);?Componentprg框架的约束prg::com组件遵循什么约束?不同类型的组件有不同的标准。要谈元器件标准,首先要对元器件进行分类。以阿里旺旺应用为例,旺旺跨端包含的组件大致可以分为以下几类:框架层:阿里系列PC应用基础组件平台相关的基础组件框架和基础组件,是阿里系列PC应用的基础,这些组件由内置的prg框架组成,从而实现快速构建PC跨终端应用的能力。应用层:旺旺业务-非UI组旺旺业务-UI组件应用层组件主要用于实现业务功能。这些组件经常需要扩展和修改,这是我们应该关注的。应用层组件,根据其技术实现,可以分为ui相关和ui独立两种,ui组件会相对复杂一些。(ps:pv层用在UI组件上,p层负责控制界面逻辑,使用纯c++实现,view层只负责绘制和操作输入,从而最大限度的复用代码,提高效率,保证业务两端的一致性。我们的ui组件都符合这个标准。我们选择了Qt作为跨终端UI框架。我们发现Qt无法实现完整的跨终端UI功能。考虑未来更换UI框架或者适配新平台的可能性,我们在UI渲染部分,也就是视图层,汇聚Qt的使用。)prg组件通用标准prg::com组件基本标准,所有prg组件均符合。每个prg组件都以...Service命名,以I...Service接口的形式暴露给外部,在C...Service中实现。服务理念:Service是一个prgcom组件,是客户端中一个独立的业务单元,是对独立业务能力的抽象。接口:IxxxService(biz/interface目录,IxxxService.h文件)实现:CxxxService(biz/xxx/service目录)获取实例:GetxxxService()使用方法:所有prg组件统一对外提供服务。用户可以通过GetxxxService()接口获取prg组件实例,然后通过IxxxService提供接口和事件使用组件。组件内部实现的CxxxService是不对外暴露的。非UI组件标准不包含ui界面组件,平台差异影响不大,内部设计基于业务需求,符合prg组件基本标准即可。UI组件标准跨终端ui组件标准主要包括mvp分层、ui生命周期管理、各种场景下的多ui组合。ui组件仍然遵循prg组件的通用标准,同时也支持prg组件的所有特性。Service:它是一个prgcom组件对象。对外使用ui组件时,直接操作服务,就像使用非ui组件一样。UI:就是整个界面,包括ui中的presenter和view,这里ui和view要分清楚。Presenter:是界面的逻辑对象。p层控制所有的业务逻辑,也控制视图的输入输出。View:是界面的渲染对象,只负责界面渲染和用户操作输入。prg框架下组件A调用组件B的UI界面:ui组件的复杂性:不同平台下ui机制不同,界面风格和操作习惯也不同。如何保证两端的业务逻辑一致?ui对象的生命周期一般由ui框架在内部进行管理。如何保证ui组件的生命周期管理不出错?如果一个组件包含多个ui怎么办?如何处理多个UI之间的并行关系,如何处理嵌套关系?ui组件的场景太多了,光是标准定义就很复杂了。在实际项目中如何实现?mvp分层结构为了更好的维护和复用ui组件,满足跨端需求,我们的ui组件都是采用mvp模式开发的。ui组件除了要遵守prgcom标准外,还需要遵守额外的约束:每个UI界面分为p-v两层,其中p层负责逻辑控制,v层负责输入输出.每个UI界面打开一个IxxxUI界面,IxxxUI代表整个UI界面,里面只有一个GetPresenter()方法。p层开启一个IxxxPresenter接口,对外调用IxxxPresenter提供的方法来操作UI界面。p层定义了v层需要实现的输入输出接口IxxxUIDelegate。该接口由v层代理实现,仅在p层可见。v层只负责渲染和输入用户操作。这样设计的好处是所有的逻辑都由p来定义和控制,p层由c++实现,可以实现跨多终端、mac和windows的统一。视图内部收敛,只实现了输入输出接口,非常轻量级。与视图相关的对象(例如QT对象)不扩散。替换view很简单,重新实现UIDelegate接口即可。通过实现UIDelegate接口的mock,可以在p层进行单元测试。生命周期管理ui组件的生命周期比一般组件要复杂一些,因为ui组件中有些对象的生命周期是由ui框架管理的。买王跨终端框架,ui选的是Qt框架,ui对象生命周期由Qt内核管理,组件生命周期由prg内核管理。ui对象创建时,组件保存ui对象的指针,用于与ui对象进行业务交互,但ui对象的生命周期由qt内部管理。因此,需要建立机制。Qt在销毁ui对象的时候,需要通知我们的组件ui对象已经销毁了。创建和调用过程:销毁过程:各种UI界面的处理单一UI界面多平面UI界面父子UI界面多层UI界面嵌套下的界面调用因为嵌套层数太多,每层都要开一个界面传将带来很大的工作量,所以提供了通用接口传递的解决方案。可以参考案例IAliwangwangChatBase.h的实现来完成接口传递。自动生成组件代码跨平台、标准化、低耦合往往意味着编码更加繁琐,也意味着实现难度大。比如我想在组件A中操作组件B,显示一个对话框,我需要写的代码是:接口CxxBView视图层的实现需要在6个对象中写代码,简直就是一场灾难!!!但是考虑到长期可维护性和跨平台性,还是得重做。因此,我们开发了一个代码生成工具,可以在上述情况下根据UI关系从模板中自动生成组件代码。限于篇幅,这里不再详述。?产生的效果这套跨端组件化方案已经在跨端千牛/跨端旺旺产品中实现。目前双品三端已经发布上??线。(目前win千牛功能>跨端千牛,win版跨端千牛暂未发布,敬请期待)两端使用体验完全一致,相同业务逻辑代码完全一致重用。双端开发成本自然减半。适合团队合作,组件拆分,协同开发效率高。组件完全解耦,可维护性大大增强。一次开发,随处使用,简单方便。可用工具自动生成组件代码,只需要关注业务逻辑,效率高,风格一致。集成集团大量基础能力,沉淀PC跨端应用组件框架,提供快速构建基于阿里PC应用的能力。回到我们选择这个方案的目标,第一是支持跨平台,第二是可维护性和可扩展性。从长远来看,可维护性对产品质量、性能和研发体验有着深远的影响。然后是良好的性能和稳定性,最后是更好的研发效率和研发体验。目前prg组件框架在前三点表现良好。第四点研发心得,由于跨多端要求严格,ui组件层数较多,开发有点繁琐。我们通过自主开发的工具生成组件代码来缓解这个问题。总的来说,prg组件跨端框架在未来3-5年可以很好的支持千牛/旺旺乃至其他阿里PC端应用的业务。跨端组件框架演进的思考?技术基础能力提升框架基础能力,如支持事件订阅优先级,支持组件链接/性能监控,增加基础能力组件等。ui组件单元测试能力,uicomponents是pv结构,ui逻辑在p层,ui逻辑可以串联简单的uidelegate实现ui单元测试。研发效率和研发体验,完善代码模板和自动化工具,实现接口级代码的自动生成/补全,进一步提升研发效率和研发体验。?可能的业务尝试走向业务组件化组件化不仅是一个技术概念,也是一个业务概念。复用带来低成本和一致性,而解耦则带来业务灵活性。技术上组件的灵活复用,可以带来灵活的业务组合和快速试用。比如旺旺系列的IM能力,可以是阿里独立的旺旺产品,也可以整合到千牛中。组件是构建块。如果你多站在商业角度思考,提供更多更好的积木,你的企业就可以快速搭建起新的大楼。共享共建pc组件库组件化不局限于团队。大家共享共建的pc业务组件库,可以更大限度的发挥组件化的价值。希望pc生意越来越好!团队介绍我们是淘宝技术部的行业和商家技术跨终端技术团队。在业务上,我们负责为千万商家打造最高效的一站式工作台千牛,为淘宝上的亿万商家和消费者提供稳定高效的终端服务。端到端消息IM服务;技术上,深耕C++跨终端和PC桌面技术(Windows&Mac),为商家和消费者提供稳定、可靠、高效的客户端产品。