现状与解决方案

最近接手的活是重构一个 Web 端文件管理器,其中包括:

  • 单文件在不同模式下 UI 渲染
  • 滚动懒加载
  • 超长列表在可视区域的懒渲染
  • 显示模式的切换
  • 文件支持不同字段排序
  • 列表适应不同页面 (桌面、回收站、收藏夹等)
  • 文件拖拽排序、移动
  • 批量选择操作
  • 不同模式的右键菜单和下拉菜单

旧代码目前存在以下问题:

  1. 不同显示模式下 单个文件 UI 渲染与交互没有分离,每增加一个显示模式,交互代码就要重复一份,导致了很多冗余代码
  2. 单个文件组件是用单一组件实现的,对于内部单元组件比较丰富的大组件而言,无疑会产生超长极难维护的代码;同时由于单个文件组件对于不同文件类型渲染存在不一致,没有组合的设计也增加了很多复杂逻辑判断 + 重复代码
  3. 列表模式 表头与文件行 UI 分离,导致样式代码重复
  4. 组件属性传递链太深、无用属性过多
  5. 某些场景过度设计、比如针对不同页面的文件列表采用 HOC 来提供文件列表属性,但是不同页面下逻辑的不一致又使得该 HOC 产生来灾难性的复杂逻辑、代码可读性、调试便利性都会随着业务迭代变得复杂
  6. 文件列表懒加载使用 react-virtualized, 体积庞大、并且迭代过程中还过度设计、封装了复杂的 API
  7. 拖拽部分 api 过多地侵入到业务组件中
  8. 其他诸如迭代过程中遗留的废代码(包括无用代码与实际不生效的业务代码)

本文并不着重于上述功能的具体实现,而是针对旧代码的问题,重新编排文件管理器组件的代码架构,也就是在这样一个应用场景下如何设计一个大型组件。

上述问题实际都是大型组件开发中,没有良好设计模式情况下容易遇到的问题。

  • 针对问题 1、2 ,将单个文件组件进行细粒度拆分,不同 UI 的渲染采用子组件的组合+样式控制来完成,交互部分则抽离在用一父组件中;UI 与交互分离、组合模式组件使得代码可读性、可维护性有明显的提高
  • 针对问题3,封装出每列样式,这样表头和文件行的列样式是一致的
  • 针对问题4,首先抽取必须的外部属性进行简化,整个文件列表则使用两个上下文:文件列表全局上下文 + 单文件上下文,基本解决属性层层传递和属性冗余的问题
  • 针对问题5,移除 HOC,不同页面 container 逻辑自治即可
  • 针对问题6,实际上 react-virtualized 作者就给出了解决方案,他指出由于开发react-virtualized 之初兼顾了过多的功能 ,导致组件庞大而复杂;作者后来开发的 react-window 和相关组件就按照单一职责原理实现,体积与速度都有提升;
  • 针对问题7,抽离拖拽组件,每个拖拽逻辑都封装成 HOC 来装饰组件,拖拽相关样式则通过类名来灌输给内部业务组件

体积优化

重构之后,vendor 体积从 875 kb 减小到 577 kb,这部分体积的减少主要是由于移除了 react-virtualized 的依赖,可以看出依赖包的选择对构建体积的影响是不小的。

业务代码则从 475 kb 减小到 421 kb,这部分体积的减少是由于移除了很多冗余、废弃的业务代码。

性能优化

重构之前,还是经过 componentShouldUpdate 优化的版本:

重构之后

思考了下,性能不佳有以下几个原因

  1. 文件列表的全局数据直接 connect 到列表组件上,没有经过 selector 进行 memoize 优化;

  2. 部分内部状态与外部属性是绑定的,导致状态变更引起不必要的多次渲染;

    修复了上述两个问题后,排序与切换模式的渲染次数有效减少:

  3. 列表上下文中的状态的变更引发除文件列表容器外内部所有子组件的渲染,拖拽的性能问题主要就是由于某个状态的变更导致内部子组件产生了非必要的重新渲染,将该状态从父容器中剔除出来,只对单个组件进行操作,可以有效减少渲染次数。同样的拖拽操作优化前后对比:

  4. 由于列表渲染时长是所有列表项目的渲染时长的叠加,当出现排序(包括拖拽)、移动等操作时,所有的项目都会重新渲染,导致在视窗比较大列表项目很多时,渲染时间明显加长。这块优化目标应该集中在单个文件的渲染速度上,特别是平铺模式下文件数量级较高的情况,暂时没有特别好的思路。

context 性能优化

当 Provider 在有状态组件内作为容器提供状态给内部组件时,即便内部组件不是消费者,也会进行重新渲染。

解决办法:

  1. 内部组件使用 PureComponent 或者 componentShouldUpdate 守护自己的渲染
  2. 用单独的组件管理 state 和 Provider,子组件写在这个组件外部

具体参考该文