经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 移动开发 » Android » 查看文章
记一个复杂组件(Filter)的从设计到开发
来源:cnblogs  作者:isNealyang  时间:2019/9/27 13:05:32  对本文有异议

此文前端框架使用 rax,全篇代码暂未开源(待开源)
原文链接地址:Nealyang/PersonalBlog

前言

貌似在面试中,你如果设计一个 react/vue 组件,貌似已经是司空见惯的问题了。本文不是理论片,更多的是自己的一步步思考和实践。文中会有很多笔者的思考过程。

从需求讨论、技术方案探讨到编码、到最终的测试,经历过了很多次的脑暴,也遇到过非常多的坑,其中有可能跟业务有关、也有可能跟框架有关,基于这些坑,又讨论了很多解决方案和非常 hack(歪门邪道)的对策。但是随着时间的推移,再回头看看当时的 hack 代码,很多都不太记得为什么这么写了,所以这里简单记录下,Filter 组件的开发过程。以便后面查询,更希望能大家一起探讨,以求得更优质的代码架构和实现思路。

由于代码编写使用基于底层 weex 的 rax 框架,所以有些坑,或许对于正在使用 react 或者 vue 的你并不会遇到,可以直接忽略

说说业务

Filter,已经常见的不可再常见的组件了,顾名思义,就是个筛选过滤器。我们先看看现有 app 上的一些 filter 展现 形式。既然做组件,我们就需要它足够的通用,足够的易于扩展。

  • 阿里拍卖的 Filter

paimai

  • 飞猪的 Filter

feizhu

在说 Filter 的业务特征之前,我们先约束下每一部分的命名,以便于你更好的阅读此文:

IMAGE

上面分别是拍卖和飞猪的 filter 页面,从这两个页面中,我们大概可以总结出关于 Filter 的一下几点业务画像:

  • 随着页面滚动,Filter 可能具有吸附能力,但是可能距离顶部存在一定的距离
  • Panel 面板多样性(点击navItem 展开的面板)
  • Panel 面板以及 navItem 都可能会有动画
  • navBar 内容可变
  • panel 面板展示形式不定
  • panel 面板内容可能非常复杂,需要考虑性能优化
  • navBar 上可能存在非 Filter 的内容(关注按钮)
  • 有的navBar 的 navItem 没有对应的 panel 面板
  • Filter 上存在影响搜索结果但是没有影响的”快排“按钮
  • filter 配置参数能够指定
  • 通过 url 传入相关筛选 id 能够初始化面板选中
  • ...

最终组件产出

由于 rax 1.0 ts+hooks 开源版本还在开发中,所以仓库链接暂时就不放上了

  • rax-pui-filter-utils : Filter 的内部工具库,仅供 Filter 开发者提供的工具库
  • rax-pui-filter-tools:配合使用 Filter 的一些工具集,比如 提高性能的 HOC 组件、占位符组件等(可用可不用,根据自己业务需求来),思考原由:并不是每一个 Filter 的使用者都需要这些功能,做成可插拔式,为了降低没必要的 bundle 大小
  • pui-filter:Filter 核心功能开发库

效果图:

console 处可见抛出的查询参数

设计与思考

前端组件架构图(初版)

组件架构图(终板)

  1. src
  2. ├─ Filter.js //Filter 最外层父容器
  3. ├─ constant.js //项目代码常量定义
  4. ├─ index.js //入口文件
  5. ├─ navbar // navBar 文件夹
  6. ├─ NavBase.js //navBar 基类 NavQuickSearch 和 NavRelatePanel 父类
  7. ├─ NavQuickSearch.js // 快速搜索(无 panel)的 navBar
  8. ├─ NavRelatePanel.js // 带有 panel 的 navBar
  9. └─ index.js // 导出文件
  10. ├─ panel
  11. └─ index.js // panel 面板组件代码
  12. └─ style.js

组件功能 Feature

  • 筛选头 UI 可动态配置扩展,支持点击动画,提供三种筛选项类型
    • RelatePanel筛选项关联Panel型,即筛选头和 Panel 是一对一关系,点击筛选头展示 Panel
    • QuickSearch筛选项快速搜索排序型,即筛选头没有对应 Panel,点击筛选头直接触发搜索
    • PureUI纯 UI占位类型,即纯 UI 放置,不涉及搜索,比如订阅按钮场景
  • 筛选面板显示隐藏统一管理,支持下拉和左滑展示隐藏动画,统一搜索回调函数
  • Filter 组件在和业务面板隔离,支持任意组件接入,业务组件里搜索变更通过 onChange(params)回调函数来触发
  • 提供了三种业务通用的面板组件
    • rax-pui-list-select,列表选择业务面板
    • rax-pui-location-select,省市区级联选择业务面板
    • rax-pui-multi-selection-panel,多选业务面板,查看组件使用文档

这里指的是 Filter 的功能 Feature,跟上文提及的 Filter 组件功能可能并不能完全覆盖,但是我们提供解决方案,组件的设计始终秉持着不侵入业务的原则,所有与业务相关均给予配置入口。

期望组件使用形式

  1. import Filter from 'rax-pui-filter';
  2. render(
  3. <Filter
  4. navConfig={[]}
  5. onChange={()=>{}}>
  6. <Filter.Panel>
  7. <业务组件1 />
  8. </Filter.Panel>
  9. <Filter.Panel>
  10. <业务组件2 />
  11. </Filter.Panel>
  12. </Filter>
  13. );

组件功能与业务需求边界划分

何为业务功能何为组件功能,这个需要具体的探讨,其实也没有严格意义上的区分。说白了,就是你买个手机,他都会送你充电器。但是。。。为什么很多手机也送手机壳(小米、华为、荣耀)但是 iPhone 却不送呢?所以到底是不是标配?

对于我们这个组件,简而言之:我们能做到的,我们都做!但是其中我们还是梳理出某些功能还是数据业务功能:

  • navBar 上每一个 navItem 展示什么文案、样式属于业务功能
  • 整个 Filter 的数据处理,包括 url 上的查询参数需要抛给对应 navItem要展示的文案也是业务功能
  • Filter 是否点击滚动到顶部也是业务功能,毕竟很多搜索页 Filter 本身置顶。而且,对于 rax 而言,不同容器滚动方式还不同(但是我们提供这样的方法给你去调用)
  • panel 面板里面数据请求、逻辑处理都是你自己的业务逻辑。Filter 只提供基本的容器能力和接口

换言之,Filter 里面任何功能都可以说为业务功能。但是我们需要提供 80%业务都需要的功能封装作为 Filter 的 Future。这就是我们的目的。

根据上面的业务功能和组件功能的区分,我们就知道在使用 Filter 的时候,你应该给我传递什么配置,以及什么方法。

Filter API

参数 说明 类型 默认值(是否必填)
navConfig 筛选头配置, 点击查看详细配置项

效果图
undefined
Array<Object> - (必填)
offsetTop Filter组件展开面板状态下距离页面顶部的高度,有两种状态:固定位置跟随页面滚动吸附置顶

固定位置 状态下距离页面顶部的高度
跟随页面滚动吸附置顶: 状态下距离页面顶部的高度

效果图
undefined
Number 0
styles 配置样式,Filter中所有样式都可使用styles集合对象来配置覆盖
styles 格式
undefined
Object {}
getStickyRef 获取 Sticky 节点的 ref 实例,用于滚动吸附场景,内部配合 pm-app-plus 容器组件点击 Filter 时自动吸附置顶

示例图
undefined
Function
keepHighlight 筛选条件改变后是否需要在筛选头保持高亮

效果图
undefined
Boolean false
clickMaskClosable 开启 mask 背景的点击隐藏 Boolean true
onChange Filter 搜索变更回调函数
签名: Function(params:Object,index:Number, urlQuery: Object) => void
参数:
params: Object 搜索参数
index:Number 触发搜索的 Panel 搜索
urlQuery:Object URL query 对象
Function
onPanelVisibleChange Panel 显示隐藏回调函数
签名: Function({ visible:Boolean, triggerIndex:Number, triggerType:String }) => void
参数:
visible:Boolean 显示隐藏标志量
triggerIndex:Number触发的筛选项索引值
triggerType:String 触发类型


triggerType详解 包含三种触发类型
Navbar:来自筛选头的点击触发
Mask:来自背景层的点击触发
Panel:来自Panel 的 onChange 回调触发
Function

Filter prop navConfig 数组配置详解

筛选项类型 type

  • RelatePanel筛选项关联Panel型,即筛选头和 Panel 是一对一关系,点击筛选头展示 Panel
  • QuickSearch筛选项快速搜索排序型,即筛选头没有对应 Panel,点击筛选头直接触发搜索
  • PureUI纯 UI占位类型,即纯 UI 放置,不涉及搜索,比如订阅按钮场景

注意 如果 navConfig 内置的UI参数不满足您的需求,请使用renderItem自定义渲染函数来控制筛选头 UI

参数 说明 类型 默认值(是否必填)
type 筛选项类型

三种类型
RelatePanel: 筛选项关联数据面板类型
QuickSearch: 筛选项快速搜索排序类型
PureUI: 纯 UI占位类型
String 'RelatePanel'
text


注意 RelatePanel类型生效
筛选头显示文案
文字溢出用...展示
String - (必填)
icons


注意 RelatePanel类型生效
筛选头 icon:normal 正常态 和 active 激活态 图标
数据格式
Object类型 :
undefined
String类型 :
undefined

效果图
undefined
Object or String -
options


注意 QuickSearch类型生效
快速搜索排序类型的数据源
数据格式
undefined
Array (必填)
optionsIndex


注意 QuickSearch类型生效
快速搜索排序类型默认选中的索引 String 0
optionsKey


注意 QuickSearch类型生效
指定快速搜索排序对应的搜索 key,用到 onChange 回调中 String 不提供默认使用当前筛选项的索引
formatText 文案格式化函数
签名:Function(text:String) => text
参数:
text: String 筛选头文案
Function (text)=>text
disabled 禁用筛选头点击 Boolean true
hasSeperator 是否展示右侧分隔符

效果图
undefined
Boolean false
hasPanel 当前筛选头是否有对应的 panel Boolean true
renderItem 自定义渲染
注意
提供的配置项无法满足你的 UI 需求时使用
签名:Function(isActive:Boolean, this:Element) => Element
参数:
isActive:Boolean 筛选头是否为激活状态
this:Element 筛选头this实例
Function -
animation 动画配置,采用内置的动画
参数说明
undefined
注意 目前只内置了一种rotate动画类型
Object
animationHook 用户自定义动画的钩子函数,内置动画无法满足需求时使用
签名:Function(refImg:Element, isActive:Boolean) => text
参数:
refImg:Element 筛选头图标的 ref 实例
isActive:Boolean 筛选头是否为激活状态
Function -

Filter.Panel API

参数 说明 类型 默认值(是否必填)
styles 配置样式
Filter中所有样式都可使用styles集合对象来配置覆盖
Object {}
displayMode Panel 展现形式:全屏、下拉
参数说明
全屏:Fullscreen
下拉:Dropdown
String 'Dropdown'
noAnimation 禁止动画 Boolean true
highPerformance 内部通过 Panel 的显示隐藏控制 panel 的 render 次数,避免不必要的 render,高性能模式下,只会在 Panel 展示 或者 展示隐藏状态变化时才会重新 render Boolean true
animation Panel 展示动画配置,内置上下左右动画
参数说明
undefined
direction 控制动画方向,分别有 updownleftright
Object

Filter 的代码使用

  • Filter 的参数配置
  1. navConfig: [
  2. {
  3. type: 'RelatePanel', // type可以不提供,默认值为'RelatePanel'
  4. text: '向下', // 配置筛选头文案
  5. icons: {
  6. // 配置 icon,分为正常形态和点击选中形态
  7. normal: '//gw.alicdn.com/tfs/TB1a7BSeY9YBuNjy0FgXXcxcXXa-27-30.png',
  8. active: '//gw.alicdn.com/tfs/TB1NDpme9CWBuNjy0FhXXb6EVXa-27-30.png',
  9. },
  10. hasSeperator: true, // 展示竖线分隔符
  11. formatText: text => text + '↓', // 筛选文案的格式化函数
  12. },
  13. {
  14. type: 'QuickSearch',
  15. optionsIndex: 0,
  16. optionsKey: 'price',
  17. options: [
  18. // 快速排序列表
  19. {
  20. text: '价格',
  21. icon: '',
  22. value: '0',
  23. },
  24. {
  25. text: '升序',
  26. icon: '//gw.alicdn.com/tfs/TB1PuVHXeL2gK0jSZFmXXc7iXXa-20-20.png',
  27. value: '1',
  28. },
  29. {
  30. text: '降序',
  31. icon: '//gw.alicdn.com/tfs/TB1a7BSeY9YBuNjy0FgXXcxcXXa-27-30.png',
  32. value: '2',
  33. },
  34. ],
  35. },
  36. {
  37. type: 'RelatePanel', // type可以不提供,默认值为'RelatePanel'
  38. text: '旋转',
  39. icons: {
  40. // 配置 icon,分为正常形态和点击选中形态
  41. normal: '//gw.alicdn.com/tfs/TB1PuVHXeL2gK0jSZFmXXc7iXXa-20-20.png',
  42. active: '//gw.alicdn.com/tfs/TB1l4lIXhv1gK0jSZFFXXb0sXXa-20-20.png',
  43. },
  44. animation: { type: 'rotate' }, // 配置动画点击后旋转图片,默认没有动画
  45. },
  46. {
  47. type: 'RelatePanel', // type可以不提供,默认值为'RelatePanel'
  48. text: '向左',
  49. },
  50. {
  51. type: 'PureUI',
  52. text: '订阅',
  53. renderItem: () => {
  54. // 渲染自定义的 UI
  55. return (
  56. <Image
  57. style={{
  58. width: 120,
  59. height: 92,
  60. }}
  61. source={{ uri: 'https://gw.alicdn.com/tfs/TB1eubQakL0gK0jSZFAXXcA9pXa-60-45.png' }}
  62. />
  63. );
  64. },
  65. },
  66. ]
  67. // ...
  68. <Filter
  69. offsetTop={100} // offsetTop = RecycleView上面的组件的高度,当前为 100
  70. navConfig={this.state.navConfig} // Filter Navbar 配置项
  71. keepHighlight={true} // 保持变更的高亮
  72. styles={styles} // 配置覆盖内置样式,大样式对象集合
  73. onChange={this.handleSearchChange}
  74. // Panel 面板显示隐藏变更事件
  75. onPanelVisibleChange={this.handlePanelVisibleChange}>
  76. <Panel highPerformance={true}>
  77. <ListSelect {...this.state.data1} />
  78. </Panel>
  79. <Panel>
  80. <LocationSelect {...this.state.data2} />
  81. </Panel>
  82. <Panel
  83. displayMode={'Fullscreen'} // 配置 Panel 全屏展示,默认为下拉展示
  84. animation={{
  85. // 动画配置
  86. timingFunction: 'cubic-bezier(0.22, 0.61, 0.36, 1)',
  87. duration: 200,
  88. direction: 'left', // 动画方向:从右往左方向滑出
  89. }}>
  90. <MultiSelect {...this.state.data3} />
  91. </Panel>
  92. </Filter>

代码运行效果图如上截图。下面,简单说下代码的实现。

核心源码展示

开源版本(Ts+hooks+lerna)还未公布,所以目前还是采用 rax 0.x 的版本编写的代码。这里只做,有坑的地方代码处理讲解。欢迎各位大佬评论留出各位想法

Filter.js

先从 render 方法看起

  1. render() {
  2. const { style = {}, styles = {}, navConfig, keepHighlight } = this.props;
  3. const { windowHeight, activeIndex } = this.state;
  4. if (!windowHeight) return null;
  5. return (
  6. <View style={[defaultStyle.container, styles.container, style]}>
  7. {this.renderPanels()}
  8. <Navbar
  9. ref={r => {
  10. this.refNavbar = r;
  11. }}
  12. navConfig={navConfig}
  13. styles={styles}
  14. keepHighlight={keepHighlight}
  15. activeIndex={activeIndex}
  16. onNavbarPress={this.handleNavbarPress}
  17. onChange={this.handleSearchChange}
  18. />
  19. </View>
  20. );
  21. }

获取一些基本配置,以及 windowHeight(屏幕高度)和 activeIndex(当前第几个item 处于 active 状态(被点开))。

之所以我们的 renderPanels 写在 NavBar 上面,是因为在 weex 中,zIndex 是不生效的。若想 A 元素在 B 元素上面,则 render 的时候,A 必须在 B 后面。这样写是为了 panel 面板展开的下拉动画,看起来是从 navBar 下面出来的。

renderPanel 方法就是渲染对应的 panel

  1. /**
  2. * 渲染 Panel
  3. */
  4. renderPanels = () => {
  5. const { activeIndex, windowHeight } = this.state;
  6. let { children } = this.props;
  7. if (!Array.isArray(children)) {
  8. children = [children];
  9. }
  10. let index = 0;
  11. return children.map(child => {
  12. let panelChild = null;
  13. let hasPanel = this.panelIndexes[index];
  14. if (!hasPanel) {
  15. index++;
  16. }
  17. if (!this.panelManager[index]) {
  18. this.panelManager[index] = {};
  19. }
  20. let injectProps = {
  21. index,
  22. visible: activeIndex === index,
  23. windowHeight,
  24. filterBarHeight: this.filterBarHeight,
  25. maxHeight: this.filterPanelMaxHeight,
  26. shouldInitialRender: this.panelManager[index].shouldInitialRender,
  27. onChange: this.handleSearchChange.bind(this, index),
  28. onNavTextChange: this.handleNavTextChange.bind(this, index),
  29. onHidePanel: this.setPanelVisible.bind(this, false, index),
  30. onMaskClick: this.handleMaskClick,
  31. disableNavbarClick: this.disableNavbarClick,
  32. };
  33. if (child.type !== Panel) {
  34. panelChild = <Panel {...injectProps}>{child}</Panel>;
  35. } else {
  36. panelChild = cloneElement(child, injectProps);
  37. }
  38. index++;
  39. return panelChild;
  40. });
  41. };

准确的说,这是一个 HOC,我们将代理、翻译传给 Filter 的影响或者 panel 面板需要使用的 props 传递给 Panel 面板。比如 onChange 回调,或者面板隐藏的回调以及当前哪一个 panel 需要展开等。

由于 Panel 的面板复杂度我们未知。为了避免不断的展开和收齐不必要的 render,我们采用 transform的方式,将面板不需要显示的面板移除屏幕外,需要展示的在移入到屏幕内部。具体可见 Panel 的render return

  1. return (
  2. <View
  3. ref={r => {
  4. this.refPanelContainer = r;
  5. }}
  6. style={[
  7. defaultStyle.panel,
  8. styles.panel,
  9. this.panelContainerStyle,
  10. {
  11. transform: `translateX(-${this.containerTransformDes})`,
  12. opacity: 0,
  13. },
  14. ]}>
  15. <View
  16. ref="mask"
  17. style={[
  18. defaultStyle.mask,
  19. styles.mask,
  20. showStyle,
  21. isWeb ? { top: 0, zIndex: -1 } : { top: 0 },
  22. ]}
  23. onClick={this.handleMaskClick}
  24. onTouchMove={this.handleMaskTouchMove}
  25. />
  26. {cloneElement(child, injectProps)}
  27. </View>
  28. );

注意: Panel 面板的坑远不止这些,比如,我们都知道,render 是最消耗页面性能的,而页面初始化进来,面板名没有展示出来(此时面板 Panel 在屏幕外),那么是否需要走 Panel 面板的 render 呢?但是目前的这种写法,Panel 组件的生命周期是会都走到的。但是如果遇到 Panel 里面需要请求数据,然后页面 url 里查询参数有 locationId=123 ,navItem 需要展示对应的地理位置.如果不渲染 Panel 如何根据 id 拿到对应的地名传递给 navItem 去展示?对,我们可以拦截 Panel 面板的 render 方法,让 Panel render null,然后别的生命周期照样运行。但是,如果 render 中用户有对 ref 的使用,那么就可能会造成难以排查的 bug。

所以最终,为了提高页面的可交互率但是又不影响页面需求的情况下,我们提供了一个可选的工具:Performance HOC 。 注意,是可选。

  1. export default function performance(Comp) {
  2. return class Performance extends Comp {
  3. static displayName = `Performance(${Comp.displayName})`;
  4. render() {
  5. const { shouldInitialRender } = this.props.panelAttributes;
  6. if (shouldInitialRender) {
  7. return super.render();
  8. } else {
  9. return <View />;
  10. }
  11. }
  12. };
  13. }

通过配置Panel 的 shouldInitialRender 属性来告诉我,是否第一次进来,拦截 render。

当然,Panel 也有很多别的坑,比如,现在 Panel 为了重复 render,将 Panel 移除屏幕外,那么,动画从上而下展开设置初始动画闪屏如何处理?

Filter 的代码就是初始化、format、检查校验各种传参,以及 Panel 和 NavBar 通信中转 比如 format、比如 handleNavbarPress

核心代码

从架构图中大概可以看出,NavBar 中通过不同的配置,展示不同的 NavBarItem 的类型,NavQuickSearch,NavRelatePanel

这里需要注意的是: NavBar 的数据是通过 Filter props 传入的,如果状态放到 Filter 也就是 NavBar 的父组件管理的话,会导致 Panel 组件不必要的渲染(虽然已经提供 Panel 层的 shouldComponentUpdate 的配置参数),同时也是为了组件设计的高内聚、低耦合,我们将传入的 props 封装到 NavBar 的 state 中,自己管理状态。

  1. constructor(props) {
  2. super(props);
  3. const navConfig = formatNavConfig(props.navConfig);
  4. this.state = {
  5. navConfig,
  6. };
  7. }
  8. // 这里我们提供内部的 formatNavConfig 方法,具体内容根据不同组件业务需求不同代码逻辑不同,这里就不展开说明了

NavBar 中还需要注意的就是被动更新:Panel 层点击后,NavBar 上文字的更新,因为这里我们利用父组件来进行 Panel 和 NavBar 的通信

  1. //Filter.js 调用 NavBar 的方法
  2. /**
  3. * 更新 Navbar 文案
  4. */
  5. handleNavTextChange = (index, navText, isChange = true) => {
  6. // Navbar 的 render 抽离到内部处理,可以减少一次 Filter.Panel 的额外 render
  7. this.asyncTask(() => {
  8. this.refNavbar.updateOptions(index, navText, isChange);
  9. });
  10. };
  11. //NavBar.js 提供给 Filter.js 调用的 updateOptions
  12. /**
  13. * 更新 navConfig,Filter 组件调用
  14. * 异步 setState 规避 rax 框架 bug: 用户在 componentDidMount 函数中调用中 this.props.onChange 回调
  15. * 重现Code:https://jsplayground.taobao.org/raxplayground/cefec50a-dfe5-4e77-a29a-af2bbfcfcda3
  16. * @param index
  17. * @param text
  18. * @param isChange
  19. */
  20. updateOptions = (index, text, isChange = true) => {
  21. setTimeout(() => {
  22. const { navConfig } = this.state;
  23. this.setState({
  24. navConfig: navConfig.map((item, i) => {
  25. if (index === i) {
  26. return {
  27. ...item,
  28. text,
  29. isChange,
  30. };
  31. }
  32. return item;
  33. }),
  34. });
  35. }, 0);
  36. };

最后 NavBar 中的 item 分为 快速搜索和带有 panel 的 NavBarItem两种,但是对于其公共功能,比如渲染的 UI 逻辑等,这里我们采用的方法是抽离 NavBase 组件,供给 NavQuickSearchNavRelatePanel 调用:

  • NavBase 部分代码
  1. renderDefaultItem = ({ text, icons, active }) => {
  2. const { formatText, hasSeperator, length, keepHighlight, isChange } = this.props;
  3. const hasChange = keepHighlight && isChange;
  4. const iconWidth = icons ? this.getStyle('navIcon').width || 18 : 0;
  5. return [
  6. <Text
  7. numberOfLines={1}
  8. style={[
  9. this.getStyle('navText'),
  10. ifElse(active || hasChange, this.getStyle('activeNavText')),
  11. { maxWidth: 750 / length - iconWidth },
  12. ]}>
  13. {ifElse(is('Function')(formatText), formatText(text), text)}
  14. </Text>,
  15. ifElse(
  16. icons,
  17. <Image
  18. ref={r => {
  19. this.refImg = r;
  20. }}
  21. style={this.getStyle('navIcon')}
  22. source={{
  23. uri: ifElse(active || hasChange, icons && icons.active, icons && icons.normal),
  24. }}
  25. />,
  26. null,
  27. ),
  28. ifElse(hasSeperator, <View style={this.navSeperatorStyle} />),
  29. ];
  30. };
  • NavRelatePanel.js
  1. export default class NavRelatePanel extends NavBase {
  2. static displayName = 'NavRelatePanel';
  3. handleClick = () => {
  4. const { disabled, onNavbarPress } = this.props;
  5. if (disabled) return false;
  6. onNavbarPress(NAV_TYPE.RelatePanel);
  7. };
  8. render() {
  9. const { renderItem, active, text, icons } = this.props;
  10. return (
  11. <View
  12. style={[this.getStyle('navItem'), ifElse(active, this.getStyle('activeNavItem'))]}
  13. onClick={this.handleClick}>
  14. {ifElse(
  15. is('Function')(renderItem),
  16. renderItem && renderItem({ active, instance: this }),
  17. this.renderDefaultItem({ text, icons, active }),
  18. )}
  19. </View>
  20. );
  21. }
  22. }

Panel 核心代码

Panel 的核心功能是对用户定义的 Panel.child 进行基本的功能添加,比如背景 mask 遮罩、动画时机的处理.

Panel 的使用:

  1. <Panel
  2. displayMode={'Fullscreen'} // 配置 Panel 全屏展示,默认为下拉展示
  3. animation={{
  4. // 动画配置
  5. timingFunction: 'cubic-bezier(0.22, 0.61, 0.36, 1)',
  6. duration: 200,
  7. direction: 'left', // 动画方向:从右往左方向滑出
  8. }}>
  9. <MultiSelect {...this.state.data3} />
  10. </Panel>

我们提供基础的动画配置,但是同时,也提供动画的 functionHook,这些都取决于动画的触发时机

  1. get animationConfig() {
  2. const { animation } = this.props;
  3. if (!animation || !is('Object')(animation)) {
  4. return PANEL_ANIMATION_CONFIG;
  5. }
  6. return Object.assign({}, PANEL_ANIMATION_CONFIG, animation);
  7. }
  8. // ...
  9. /**
  10. * 执行动画
  11. * @param nextProps
  12. */
  13. componentWillReceiveProps(nextProps) {
  14. if (nextProps.visible !== this.props.visible) {
  15. if (nextProps.visible) {
  16. setNativeProps(findDOMNode(this.refPanelContainer), {
  17. style: {
  18. transform: `translateX(-${rem2px(750)})`,
  19. },
  20. });
  21. this.props.disableNavbarClick(true);
  22. this.enterAnimate(this.currentChildref, () => {
  23. this.props.disableNavbarClick(false);
  24. });
  25. this.handleMaskAnimate(true);
  26. } else {
  27. this.handleMaskAnimate(false);
  28. this.props.disableNavbarClick(true);
  29. this.leaveAnimate(this.currentChildref, () => {
  30. this.props.disableNavbarClick(false);
  31. setNativeProps(findDOMNode(this.refPanelContainer), {
  32. style: {
  33. transform: 'translateX(0)',
  34. },
  35. });
  36. });
  37. }
  38. }
  39. }

由于动画的执行需要时间,所以这个时间段,我们应该给 Filter 中的 NavBar 加锁 ,锁的概念也同样提供给用户,毕竟业务逻辑我们是不会侵入的,在上一次的搜索没有结果返回时候,应该给 NavBar 加锁,禁止再次点击(虽然用户可以再 onchange 回调函数中处理,但是作为组件,同样应该考虑并且提供这个能力),同样对于动画也是如此,在该动画正在执行的时候,应该禁止 NavBar 的再次点击。上面的动画配置效果如下:

Panel 中还有核心的处理或许就是关于动画时机的处理。比如在触发动画前,我们需要设置动画初始状态,但是如若如下写法,会出现 Panel 闪动的现象,毕竟我们通过第二次的事件轮训回来才执行初始化,所以这里,如果用户配置启动动画,那么我们需要在 Panel 的最外层添加一个可见的 flag:默认进来 opacity 设置为 0,当动画初始状态设置完毕后,在将最外层容器的 opacity 设置为 1,其实 Panel 还是闪了一下,只是你看不到而已。

  1. // 设置动画初始样式
  2. setTimeout(() => {
  3. setNativeProps(node, {
  4. style: {
  5. transform: !visible ? 'translate(0, 0)' : v,
  6. },
  7. });
  8. }, 0);
  9. // 执行动画
  10. setTimeout(() => {
  11. transition(
  12. node,
  13. {
  14. transform: visible ? 'translate(0, 0)' : v,
  15. },
  16. {
  17. timingFunction: timingFunction,
  18. duration: duration,
  19. delay: 0,
  20. },
  21. cb,
  22. );
  23. }, 50);

设置动画初始化样式中添加:

  1. setNativeProps(findDOMNode(this.refPanelContainer), {
  2. style: {
  3. opacity: 1,
  4. },
  5. });

结束语

Filter 的组件看似简单,但是如果想写一个市场上较为通用和广泛的 Filter 组件,不仅仅是组件的颗粒度、耦合度和性能需要考虑,更多的是其中还是有太多的业务逻辑需要去思考。对于目前的初版(还未修改成正式开源版),已经基本涵盖了目前我们能够想到的业务场景,也已经有相关业务落地使用。

当然,对于如果是直接放到业务中使用而不作为开源组件的话,我们可已经 Panel下的 child 通过 renderPortal 降低层级,通过 EventBus 或者 redux、mobx 等管理数据状态。那样会让整个代码逻辑看起来清晰很多。但是为了降低bundle 大小,我们尽可能的减少通用包的使用以及第三方插件的依赖。

关于文章中没有提及的想法或者对于这些Filter业务需求(坑)你有更好的处理方法和想法都欢迎在评论区交流~

学习交流

关注公众号: 【全栈前端精选】 每日获取好文推荐。

公众号内回复 【1】,加入全栈前端学习群,一起交流。

原文链接:http://www.cnblogs.com/Nealyang/p/11596943.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号