美文网首页
复杂页面如何拆解?——页面元素组件化方案

复杂页面如何拆解?——页面元素组件化方案

作者: kuaishou | 来源:发表于2018-12-26 10:16 被阅读0次

    "拆解不同的页面元素为组件,通过组件组合的方式构建页面"

    在版本迭代过程中,随着功能越来越丰富,代码也会越来越多。面对一个“巨无霸”页面,我们如何拆解?拆解后如何协作、如何通信?
    本文介绍一种使用组件化方案构建复杂页面的设计思路,以及快手如何应用这个思路重构个人中心页面的实例。

    背景介绍

    随着业务的发展,项目中的一些核心页面会变得越来越庞大。过大的类本身就散发着坏的代码味道,大量的代码挤在一起,众多复杂的逻辑相互交织,开发和维护变得愈发困难。如果不同业务线同时修改同一个复杂页面,会带来大量的冲突和众多if...else判断。

    当一个ViewController变成拥有几千行的庞然大物的时候,在开发和迭代过程中,常常会遇到如下的一些困难:

    • 类过大,修复bug不容易定位问题
    • 内部逻辑相互依赖,互相关联,新增需求可能破坏原有功能
    • 页面样式复杂,且有依赖关系
    • 不同的业务操作可能会操作同一个View,容易出现展示错误

    这样的页面就好像一个大抽屉,打开之后堆满了各种代码。我们下面要做的,就是利用一些“收纳盒”,把有关联的东西都放在一个个小盒子里。

    定义组件

    组件是一个个独立的,可复用的部件。对外,组件提供一个绘制好的view;对内,组件管理自己内部的页面元素和业务逻辑。通过添加子组件的操作,组件之间被组织起来,形成一棵组件树。之后我们便可以通过这棵组件树做内部消息的传递。

    可以把组件定义成协议,这样,无论是View,ViewController,还是NSObject,都可以通过实现协议,变成组件。定义如下

    @protocol Component <NSObject>
    
    @property (nonatomic, readonly) UIView *view;
    @property (nonatomic, weak) id<Component> superComponent;
    @property (nonatomic, strong) NSMutableArray<id<Component>> *subComponents;
    
    - (void)addComponent:(id<Component>)component;
    - (void)removeComponent:(id<Component>)component;
    - (void)removeFromSuperComponent;
    
    

    “各家自扫门前雪”,组件只专注于自己这一块视图的绘制,当然,它也可以通过添加子组件的方式,将自己视图内的一部分区域“外包”给别的组件管理。

    如何拆解和形成组件树

    view本身有一个树状的层级结构,当其中的一些view是由组件提供出来的时候,这些组件便形成了组件树。

    组件树

    拆解的过程遵循自上而下,化整为零的原则。分析页面元素之间的关系,将相对集中的元素合并在一起,形成组件。拆解的过程中也要遵循适度原则:组件不能太大,对于过大的组件,可以在迭代开发中逐渐拆解;组件也不适宜太小,琐碎或者层级过深的结构都不利于代码的阅读和理解,会增加未来维护的成本。

    这里有个问题,在使用组件的时候,如果既要添加组件的view,比如

    [self addSubview:component.view]
    

    又要操作组件的父子关系,比如

    [self addComponent:component]
    

    就显得有些啰嗦。这里,我们通过重写view的一些生命周期方法,在组件的view被添加的同时,自动构建起组件的父子关系。
    例如

    - (void)willMoveToSuperview:(UIView *)newSuperview {
        [super willMoveToSuperview:newSuperview];
        id<Component> component = self.component;
        
        if (!component) {
            return;
        }
        
        if (newSuperview) {
            [newSuperview.component addComponent:component];
        } else {
            [component removeFromSuperComponent];
        }
    }
    

    相似的,didMoveToSuperview,didMoveToWindow也有一些组件父子关系自动构建的方法,这里就不一一列举了。这样,在使用组件的时候,只需要添加组件的view,就可以自动构建出组件树的层级结构了。

    如何通信

    还是那个大抽屉的比喻,当所有东西都放在一起的时候,虽然杂乱了一些,但是彼此的访问却非常顺畅:需要用到什么状态,什么方法,直接调用就好了。拆解成组件之后,组件之间就增加了通信的成本。下面是几种组件间通信方式

    父子组件

    使用直接通信的方式。父组件持有并使用子组件的视图,所以父组件知道子组件的类型,可以通过子组件的构造函数,设置属性或者调用方法,直接传递消息给子组件。子组件虽然不知道自己父组件的具体类型,但可以通过block或者delegate的方式,将自己内部的消息转发给使用自己的父组件。

    跨层级通信

    父组件 => 子组件 => ... => 子组件

    如果按照上面父子组件通信方式层层传递,比较繁琐,胶水代码也较多。但是如果放开通信限制,允许任意组件之间进行网状通信,工程的复杂度会随着组件数量的增加,爆炸性增长。因此,我们希望提供一种单向的,有明确数据类型的状态同步机制。
    本次实践借鉴了ContextProviderConsumer的模式,即组件树上的某一个节点作为状态的提供者(Provider),它子树上的组件,可以作为消费者(Consumer)去注册监听这个提供者状态的变化,当状态发生变化的时候,消费者可以收到消息。

    概括来说

    • Provider 提供共享状态,负责更新状态
    • Consumer 监听Provider状态的变化,对共享状态只读

    下面是举一个传递用户信息的Provider和Consumer的例子

    @protocol UserProfileProvider <NSObject>
    
    @property (nonatomic, strong) UserProfile *userProfile;
    @property (nonatomic, assign) BOOL isMyProfile;
    
    - (void)updateFollowerCount:(NSUInteger)followerCount;
    
    @end
    
    @protocol UserProfileConsumer <NSObject>
    
    @property (nonatomic, weak) id<UserProfileProvider> userProfileProvider;
    
    @optional
    - (void)userProfileDidUpdate:(NSDictionary<NSKeyValueChangeKey, id> *)change;
    - (void)isMyProfileDidUpdate:(NSDictionary<NSKeyValueChangeKey, id> *)change;
    
    @end
    
    
    provider & consumer

    有了协议声明,那如何建立起来状态变化的监听呢?在具体实现上,我们采用了kvo的方式,即在构建组件树的同时,runtime去判断这个组件是否是某一Context的Provider或者Consumer。如果判断成功,则建立相应的kvo监听。这样,在Provider组件修改自身某一状态的时候,监听它的Consumer便可以收到状态变化的消息。

    如何协作

    对于更复杂的,需要组件间联动来完成某一功能的需求,比如点击一个按钮,带来页面内不同层级的几个组件的UI变化。可以通过上面介绍的ContextProviderConsumer模式,设计一个状态,当子组件的按钮被点击之后,发送消息给Provider,Provider更改状态,之后所有Consumer收到状态变化的消息,自己处理自身的变化。

    具体实例

    快手iOS客户端的个人中心页,就是这样一个复杂的页面。包含了游戏、商业化、社交链、课程等众多功能入口,同时拥有作品,说说,私密,收藏,喜欢和音乐六大Tab,在
    很多地方又需要承担ab测试的分支样式和逻辑。

    快手个人中心页

    随着新需求的不断增加,个人中心页变成了一个几千行的大类。重构过程运用了上面介绍的组件化方案。大体上,页面主要被分解为导航组件和列表组件,列表组件又包含了背景图组件,用户信息组件以及各个Tab组件。

    结构分解

    具体拆解如下图

    个人中心页组件结构

    在实践过程中,页面的组件树上可能存在多个Context。快手个人中心页重构过程中,就建立了用户信息,Table滑动位置,音乐,说说等多个状态共享通道。另外,根组件通常承担了状态提供者的角色,也承担了较多业务逻辑。

    总结

    • 通过页面元素组件化的方式,可以有效的拆解复杂页面,降低耦合
    • 封装组件树的构建过程,在添加组件view的同时,在内部构建了父子关系
    • 利用组件的树状结构,借助ContextProviderConsumer做跨层级的组件通信

    相关文章

      网友评论

          本文标题:复杂页面如何拆解?——页面元素组件化方案

          本文链接:https://www.haomeiwen.com/subject/dsoflqtx.html