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

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

作者: 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做跨层级的组件通信

相关文章

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

    "拆解不同的页面元素为组件,通过组件组合的方式构建页面"在版本迭代过程中,随着功能越来越丰富,代码也会越来越多。面...

  • vue 的组件与父子组件传值知识点

    组件化组件化就是一种拆解复杂问题的思路,一般是将复杂的页面拆解成一个个小的可复用的组件,方便组织和管理,扩展性强。...

  • 第5.61章:自定义Header组件

    创建一个新页面,作为组件页面,命名首字母大写 引入要使用的组件页面,调用页面,实现组件化

  • (21)打鸡儿教你Vue.js

    组件化思想: 组件化实现功能模块的复用 高执行效率 开发单页面复杂应用 组件状态管理(vuex) 多组件的混合使用...

  • 新手如何学自动化测试四(selenium 命令之验证页面元素)

    根据上一节计划,这里我们来学习一下如何验证页面元素。 验证页面元素 验证页面上的UI元素,是你在自动化测试案例过程...

  • 页面结构复杂时如何拆解vue模块?

    在实际写页面的时候,如果一个页面的结构比较复杂,那么我们着一个页面的前端代码的量就会很大,那么我们如何拆解一个页面...

  • 工作中的一些规范总结

    Web前端页面规范化和交互规范 1.组件划分:组件分为公共组件、页面组件 1.1公共组件 公共组件一般是页面公用...

  • vue

    1、什么是组件化、有什么好处、vue如何创建组件、vue组件之间如何通信 什么是组件化。任何一个页面我们都可以抽象...

  • go处理秒杀系统

    秒杀系统的定制: 前端方案 浏览器端(js):页面静态化:将活动页面上的所有可以静态的元素全部静态化,并尽量减少动...

  • 路由处理

    基础功能设置 初始化路由页面组件 在views目录中创建路由页面组件,设置方式: 每个页面组件设置独立的目录,内部...

网友评论

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

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