3

跨桌面端之组件化实践

 2 years ago
source link: https://www.51cto.com/article/715516.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

windows千牛功能很丰富,mac千牛什么时候可以把能力对齐?

相信所有跨平台应用,都有遇到过这样的窘境。由于平台差异的复杂性,维护多端产品成本非常高,且常常存在多端体验不一致的问题。情况就是这样,而我们团队维护了pc千牛和pc旺旺2款跨端产品,在效能和体验的双重压力之下,搭建一个多端统一的pc应用跨平台开发框架势在必行。

本文主要介绍了千牛PC跨端框架中,我们关于组件化部分的思考、方案选择、遇到的一些问题和解法。

所谓框架,它既是一个“框子”,有一定的约束性,也是一个“架子”,有一定的支撑性。IT语境中的框架,特指为解决一个开放性问题而设计的具有一定约束性的支撑结构。在此结构上可以根据具体   问题扩展、安插更多的组成部分,从而更迅速和方便地构建完整的解决问题的方案。

为什么要做组件化?

跨端框架为什么选择做组件化?

框架本身一般不能直接解决某个具体问题,但为解决问题的相关组件提供了一些衔接、组合的基础能力。

框架的科学性、易用性,直接决定了研发效率和产品质量。

组件化是用来解决框架功能扩展、复用的一种非常合适的技术方案。

而用组件化模式设计的一个应用框架,一般具备以下特性:

  1. 极好的扩展性
  2. 极好的复用性
  3. 灵活度高,可以很方便组装或下线功能
  4. 修改功能,影响范围很小
  5. 非常适合团队分工协作

这些优点,每一项都是我们梦寐以求的,所以说组件化对我们几乎是必然选择。

组件化是什么?

组件化是指解耦复杂系统时将多个功能模块拆分、重组的过程,有多种属性、状态反映其内部特性。

举个例子,你要造一个汽车,但是发现汽车实在太复杂了,很难实现,于是:

  1. 你把汽车拆分成了底盘,发动机,变速箱,轮子等模块,定义好了他们各自的职责。
  2. 然后你找来小伙伴帮忙,先约定好了组件标准,再让每个人实现其中一个独立模块。
  3. 你还需要一个控制系统,能够让这些模块互相配合,协同工作。
  4. 由于大家都按照一个标准来开发,这些模块很容易被组装到一起,进行管理和控制。
  5. 这样,你们实现了一个汽车。

这里的一个功能模块,就是一个组件,用来控制组件协作运行的系统,就是组件框架。

组件框架需要解决的几个问题:

  1. 如何发现组件
  2. 如何管理组件的生命周期
  3. 如何组件间调用
  4. 提供的公共基础能力
图片

怎么选择组件化方案?

组件化的落地方案很多,我们怎么选择适合自己的技术方案?

业界的组件化方案很多,例如windows下的com组件,andriod下的ARouter组件,基于消息总线的ths组件,千牛自研的prg::com组件,还有一些基于rpc框架,更宽泛意义上组件化(微服务)。

在我看来,组件化方案没有最好的,只有相对合适的。根据业务场景,选择一个满足当前业务需要,又能适当照顾到未来发展需要,好用好维护的方案就可以。

这里提供一些组件化方案选型一些可参考的维度:

  1. 跨编程语言
图片

在跨端千牛的场景下,我们的诉求优先级是:

  1. 首先必须是支持跨平台的,
  2. 其次是良好的可维护性,长期来看,可维护性对产品质量、效能和研发体验都影响深远。
  3. 然后是良好的性能和稳定性,
  4. 最后是较好的研发效能和研发体验。

这里我们主要对比了ths组件和prg::com组件方案:

  1. ths组件:ths方案类似于一个rpc调用框架,所有调用以消息的形式在总线上传递,其运行时隔离&有中心节点切面,但其接口可维护性较差,无法在编译期发现问题。ths方案更适合跨团队场景,或开放场景。
  2. prg::com组件:prg::com组件类似于微软的com组件,但它支持了跨平台,并对com接口调用方式进行了优化,调用方便。其接口的可维护性较佳,编译时就可以发现接口兼容性问题,性能也非常不错,十分适用于团队内部的组件化场景。

最终我们选用了自研的prg::com作为跨端框架组件化的技术方案,下面具体介绍一下这个方案。

跨端组件化实践

组件化方案,包含框架能力和组件约束两部分。

框架设计是否科学,组件约束下是否易开发、易使用、易维护,是组件化方案要考量的核心因素。

  1. 组件框架,提供了组件运行的基础能力,主要包括:组件发现机制组件生命管理组件间通信其他公共基础能力
  2. 组件约束,定义了组件开发时需要遵循的标准,其主要目的包括了:支持组件在prg框架上运行,例如组件都继承自prg::com对象,并需要完成I接口注册。支持组件跨平台,例如ui组件需要遵守mvp分层,在替换ui渲染层时,能确保做到业务逻辑多端一致。为了便于团队协作,例如文件结构、代码分层、命名规则等,使用相同范式去开发和使用组件。……

组件约束是根据组件的类型、具体使用场景等因素,分别进行定义的,不同组件的标准并不完全相同。

例如ui组件和非ui组件的标准就有很大的不同。

后面介绍下prg框架,及我们在各种场景下定义的组件约束条件。

▐  prg框架

  • 组件发现机制

prg框架利用模板技术,通过打包时扫描dll生成配置、加载dll时静态注册组件,实现了一套组件发现机制。

prg框架的组件发现机制,依赖于id注册,组件对外只暴露类id和I接口,实现了组件间完全去依赖。

先来看一下示意代码:

// 定义一个prg::com组件
class IxxxService;
DEFINE_IID(IxxxService, "{4E6A382D-1FDA-49C6-8521-E284DA7B71CC}")
DEFINE_CLSID(xxxService, "{D1A52645-7587-4885-ABFD-323BA62905F5}")

// 创建这个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);

它的实现原理是:

  1. 打包时,扫描目录下所有dll,遍历调用 GetPrgCOMFactory接口,生成组件配置xml。
  2. 创建对象时,通过xml配置,找到并load对应的dll。
  3. 加载dll时,会创建prg::CPrgCOMObjectRuntime<T>静态变量g_prgRuntime,并在构造时向PrgCOMFactory注册clsid到this的映射关系。
  4. 根据clsid,在PrgCOMFactory中找到对应的g_prgRuntime变量,调用CreateInstance静态方法,由于g_prgRuntime变量是带了T类型信息的,就可以创建出对应的T对象。

这里利用了c++模版技术和静态注册技术,巧妙地完成组件解耦,解决了依赖问题和跨模块调用问题。

  • 组件生命管理

prg组件支持无感跨模块创建、使用、释放对象,真正做到了一次开发,到处使用,开箱即用。

prg组件使用scoped_refptr引用计数管理内存,使用者不需要自行管理内存。

prg框架支持跨dll/dylib创建、使用、释放对象,对使用者来说dll/dylib是完全无感的,指定要创建的对象类型,接口类型,实例名称,就可以直接开始使用这个接口了,非常丝滑。

class IxxxService : public prg::IPrgCOMRefCounted
{
public:
    base::event<void()> onDataChanged;
public:
    virtual bool GetData(const std::string& data) = 0;
}

// 创建prg::com组件新实例
scoped_refptr<IxxxService> spInterface;
prg::PrgCOMCreateInstance(c_uuidof(xxxService),  spInterface);

// 获取prg::com组件(没有则create,prg框架内部会保存一份引用)
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);

我们一般会将组件获取封装成像下面这样的接口,对于使用者来说,调接口就像调用自己的代码一样方便。

/// 组件头文件
inline scoped_refptr<IxxxService> GetIxxxService()
{
    scoped_refptr<IxxxService> spInterface;
    prg::PrgCOMGetInstance(c_uuidof(UIAppGuideWidget), "", spInterface);
    return spInterface;
}

/////////////////////////////////////////////////////////////////////////
// 其他组件直接调用接口
std::string data;
GetIxxxService()->GetData(data);
  • 组件间通信

prg组件的接口调用和事件订阅。

接口调用

prg组件接口调用与com组件类似,区别在于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);
// 调用组件方法
spInterface->GetData(callback);

事件订阅

事件订阅派发,依赖base::event实现,是典型的观察者模式。当事件触发时,按照注册顺序挨个调用观察者的base::callback,可以非常容易的完成复杂流程串联。这里的event是实例级别的,配合prg的账号隔离能力,可以很好的解决多账号业务的事件派发问题。但目前base::event暂不支持按优先级注册派发。

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组件要遵循什么约束条件?

不同类型的组件,标准是不一样的,要说组件标准,首先要对组件进行分类。

以阿里旺旺应用为例,跨端旺旺包含的组件,大致可以分成以下几类:

  1. ali系pc应用基础组件
  2. 平台相关基础组件

框架和基础组件,是阿里系pc应用基座,这些组件由prg框架内置,从而实现快速搭建pc跨端应用的能力。

  1. 旺旺业务-非UI组
  2. 旺旺业务-UI组件

应用层组件,主要用来实现业务功能,这部分组件经常要进行扩展和修改,是我们要重点关注的。

应用层组件,根据其技术实现,又可以分成ui相关和ui无关两种,ui组件会相对更加复杂。

(ps:UI组件上采用pv分层,p层负责控制界面逻辑,使用纯c++实现,view层只负责绘制和操作输入,这样在最大程度复用代码,提高效率的同时,保证业务双端一致。我们的ui组件都遵从这个标准。我们选用了Qt作为跨端UI框架,我们发现,Qt并不能做到UI功能完全跨端,考虑到后续替换UI框架或适配新平台的可能性,我们把Qt的使用范围收敛在UI渲染部分,即view层。)

图片

prg组件通用标准

prg::com组件基本标准,所有的prg组件都遵守。

每个prg组件,都以…Service命名,以 I…Service接口的方式对外暴露,在C…Service里实现。

Service概念:Service即是prgcom组件,是客户端内的独立业务单元,是对独立业务能力的抽象。

接口:IxxxService(在biz/interface目录,IxxxService.h文件)

实现:CxxxService(在biz/xxx/service目录)

获取实例:GetxxxService()

使用方法:

  1. 所有prg组件对外提供服务的方式是统一的。
  2. 使用者可以通过GetxxxService()接口获取到prg组件实例,然后通过IxxxService提供接口和事件使用组件。
  3. 组件内部实现CxxxService不对外暴露。
图片

非UI组件标准

不包含ui界面的组件,平台差异影响较小,内部按业务需要设计,遵守prg组件基本标准即可。

  • UI组件标准

跨端ui组件的标准,主要包括了mvp分层,ui生命周期管理,以及各种场景下的多ui组合等。

ui组件依然遵从prg组件的通用标准,也支持prg组件的所有特性。

Service:是一个prgcom组件对象,外部使用ui组件时,直接操作service,就像使用非ui组件一样。

UI:是界面整体,ui里包含presenter、view, 这里ui和view要区分清楚。

Presenter:是界面的逻辑对象,p层控制了所有业务逻辑,也控制view的输入输出。

View:是界面的渲染对象,只负责界面渲染和用户操作输入。

在prg框架下,A组件调用组件B的UI接口:

图片

ui组件的复杂性:

  1. 不同平台下的ui机制不相同,界面风格和操作习惯也不同,如何确保双端业务逻辑一致?
  2. ui对象的生命周期一般由ui框架内部管理,如何确保ui组件的生命周期管理不出问题?
  3. 如果一个组件里,包含多个ui怎么处理?多个ui之间并列关系怎么处理,嵌套关系又怎么处理?
  4. ui组件的场景太多,光标准定义就很复杂,如何在实际项目中落地实施?

mvp分层结构

为了更好的维护和复用ui组件,并满足跨端诉求,我们的ui组件都采用mvp模式进行开发。

ui组件除了遵从prgcom的标准之外,还需遵守额外的约束:

  1. 每个UI界面内部,分为p-v两层,其中p层负责逻辑控制,v层负责输入输出。
  2. 每个UI界面,开放一个IxxxUI接口,IxxxUI代表UI界面整体,内部只包含一个GetPresenter()方法。
  3. p层开放一个IxxxPresenter接口,外部调用IxxxPresenter提供的方法来操作这个UI界面。
  4. p层定义了v层需实现的输入输出接口IxxxUIDelegate,这个接口由v层代理实现,只在p层可见。
  5. v层只负责实现渲染和输入用户操作。
图片

这种设计的好处,主要在于:

  1. 所有的逻辑由p定义和控制,p层由c++实现,可以实现跨多端,mac和windows统一。
  2. view被收敛在内部,仅实现了输入输出接口,非常轻量。view相关的对象(如QT对象)不会扩散。
  3. 替换view简单,只需重新实现UIDelegate接口。
  4. 可以通过实现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,显示一个对话框,我需要写的代码有:

  1. IxxBService 组件接口
  2. CxxBService 组件实现
  3. IxxBPresenter P层接口
  4. CxxBPresenter p层实现
  5. IxxBPresenter::IxxBViewDelegate p层定义的ViewDelegate接口
  6. CxxBView view层实现

要在6个对象里写代码,这简直就是一个灾难!!!但是考虑到长期的可维护性、跨平台,又必须这么做。

于是,我们开发了一个代码生成工具,根据上面各种情况下的ui关系,可以从模板自动生成组件代码。

由于篇幅限制,这里不具体展开。

▐  产生的效果

这套跨端组件化方案,已在跨端千牛/跨端旺旺产品中落地,目前双产品三端已经发布上线。

(目前win千牛功能 > 跨端千牛,跨端千牛的win版本尚未发布,敬请期待)

  1. 双端完全一致的使用体验,完全复用相同业务逻辑代码。
  2. 双端开发成本天然降低一半。
  3. 适合团队协作,组件分拆,协同开发的效率高。
  4. 组件间完全解耦,可维护性大大增强。一次开发,到处使用,简单方便。
  5. 可用工具自动生成组件代码,只需关注业务逻辑,效率高,风格一致。
  6. 集成了大量集团基础能力,沉淀了pc跨端应用组件化框架,提供快速搭建阿里系pc应用能力。

再回到我们选择这个方案时的目标,

  1. 首先是支持跨平台,
  2. 其次是可维护性、扩展性,长期来看,可维护性对产品质量、效能和研发体验都影响深远。
  3. 然后是良好的性能和稳定性,
  4. 最后是较好的研发效能和研发体验。

目前来看,prg组件框架在前3点上表现出色,在第4点研发体验上,由于跨多端的严苛要求,ui组件分层较多,开发略显繁琐,我们通过自研组件代码生成工具,缓解了这一问题。

总的来说,prg组件跨端框架可以在未来的3-5年里,很好的支撑起千牛/旺旺,甚至其他阿里系pc应用的业务。

跨端组件框架的演进思考

▐  技术基础能力完善

框架基础能力,例如支持事件订阅优先级、支持组件链路/性能监控、增加基础能力组件等。

ui组件单元测试能力,ui组件都是pv结构的,ui逻辑都在p层,用简单的uidelegate即可串联ui逻辑,实现ui单元测试。

研发效能和研发体验,完善代码模板和自动化工具 ,实现接口级别代码自动生成/补全,进一步提升研发效率和研发体验。

▐  业务上可能的尝试

走向业务的组件化

组件化不光是技术概念,也是业务概念。复用带来了低成本和一致性,解耦则带来了业务的灵活性。

组件在技术上的灵活复用,能带来的是业务上的灵活组合,快速尝试。

例如旺旺系的IM能力,它可以是独立的阿里旺旺产品,也可以集成到千牛里。组件就是积木,更多从业务角度去思考,提供更多更好用的积木,业务就能快速搭建出一个新大楼。

  • 共享、共建pc组件库

组件化不局限于团队内,大家一起共享、共建的pc业务组件库,才是更大程度发挥出组件化的价值。

希望pc业务越来越好!

团队介绍

我们是大淘宝技术部行业与商家技术跨终端技术团队,业务上负责为千万级商家打造最高效的一站式工作台千牛,为淘宝上亿商家和消费者提供稳定高效的端到端消息IM服务;技术上深耕C++跨终端及PC桌面端技术(Windows&Mac),为商家,消费者提供稳定,可靠,高效的客户端产品。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK