vue 筛选组件_记一个复杂组件(Filter)的从设计到开发

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

前言

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

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

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

说说业务

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

  • 阿里拍卖的 Filter

c713843e39106ae628680e4e1d66cb0b.gif
  • 飞猪的 Filter

ea6d92052ed36b435ec0415f80ff8d93.gif

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

38dd4dd745ead5e2da17d70fb57365db.png

上面分别是拍卖和飞猪的 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 核心功能开发库

效果图:

de0e186093d810fa8abfa3249ceb04bf.gif

console 处可见抛出的查询参数

设计与思考

前端组件架构图(初版)

2341c2e17b7f431db3ad246eaf0ff05a.png

组件架构图(终板)

4c261968325a2137d7b6ae7df3304a63.png
src
├─ Filter.js    //Filter 最外层父容器
├─ constant.js  //项目代码常量定义
├─ index.js     //入口文件
├─ navbar       // navBar 文件夹
│    ├─ NavBase.js    //navBar 基类 NavQuickSearch 和 NavRelatePanel 父类
│    ├─ NavQuickSearch.js   // 快速搜索(无 panel)的 navBar
│    ├─ NavRelatePanel.js   // 带有 panel 的 navBar
│    └─ index.js  // 导出文件
├─ panel
│    └─ index.js  // panel 面板组件代码
└─ 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 组件功能可能并不能完全覆盖,但是我们提供解决方案,组件的设计始终秉持着不侵入业务的原则,所有与业务相关均给予配置入口。

期望组件使用形式

 import Filter from 'rax-pui-filter';render(<FilternavConfig={[]}onChange={()=>{}}><Filter.Panel><业务组件1 /></Filter.Panel><Filter.Panel><业务组件2 /></Filter.Panel></Filter>);

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

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

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

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

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

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

Filter API

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

效果图

015f4384be6d5ad9f072ea5a3625a3b2.png

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

固定位置 状态下距离页面顶部的高度

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

效果图

f01103fd1b5074ef9bd7d9319de6fc85.png

Number0styles配置样式,Filter中所有样式都可使用styles集合对象来配置覆盖

styles 格式

7aba15fb398bf9dbe75e330391ceac3e.png

Object{}getStickyRef获取 Sticky 节点的 ref 实例,用于滚动吸附场景,内部配合 pm-app-plus 容器组件点击 Filter 时自动吸附置顶

示例图

885b9879146b7ee7c2348d988f333e86.png

FunctionkeepHighlight筛选条件改变后是否需要在筛选头保持高亮

效果图

f3471ab1d042622ca0659a8442f1c287.png

BooleanfalseclickMaskClosable开启 mask 背景的点击隐藏BooleantrueonChangeFilter 搜索变更回调函数

签名:Function(params:Object,index:Number, urlQuery: Object) => void

参数:

params: Object 搜索参数

index:Number 触发搜索的 Panel 搜索

urlQuery:Object URL query 对象

FunctiononPanelVisibleChangePanel 显示隐藏回调函数

签名:Function({ visible:Boolean, triggerIndex:Number, triggerType:String }) => void

参数:

visible:Boolean 显示隐藏标志量

triggerIndex:Number触发的筛选项索引值

triggerType:String 触发类型

triggerType详解 包含三种触发类型

Navbar:来自筛选头的点击触发

Mask:来自背景层的点击触发

Panel:来自Panel 的 onChange 回调触发Function

Filter prop navConfig 数组配置详解

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类型 :

1055584656713e16862e71fa63141ad0.png

String类型 :

ad359f800958129c382f90c16de6590f.png

效果图

044972404692314618f046d18ea2b5e9.png

Object or String-options

注意QuickSearch类型生效快速搜索排序类型的数据源

数据格式

d143792b2fe550155f1ce05f0eb2cf22.png

Array(必填)optionsIndex

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

注意QuickSearch类型生效指定快速搜索排序对应的搜索 key,用到 onChange 回调中String不提供默认使用当前筛选项的索引formatText文案格式化函数

签名:Function(text:String) => text

参数:

text: String 筛选头文案Function(text)=>textdisabled禁用筛选头点击BooleantruehasSeperator是否展示右侧分隔符

效果图

6c450fcd6380c3251eefcfc72556ff30.png

BooleanfalsehasPanel当前筛选头是否有对应的 panelBooleantruerenderItem自定义渲染

注意

提供的配置项无法满足你的 UI 需求时使用

签名:Function(isActive:Boolean, this:Element) => Element

参数:

isActive:Boolean 筛选头是否为激活状态

this:Element 筛选头this实例Function-animation动画配置,采用内置的动画

参数说明

e5de62bdeafaef6eaca42ce805f4d0b4.png

注意 目前只内置了一种rotate动画类型ObjectanimationHook用户自定义动画的钩子函数,内置动画无法满足需求时使用

签名:Function(refImg:Element, isActive:Boolean) => text

参数:

refImg:Element 筛选头图标的 ref 实例

isActive:Boolean 筛选头是否为激活状态Function-

Filter.Panel API

参数说明类型默认值(是否必填)styles配置样式

Filter中所有样式都可使用styles集合对象来配置覆盖Object{}displayModePanel 展现形式:全屏、下拉

参数说明

全屏:Fullscreen

下拉:DropdownString'Dropdown'noAnimation禁止动画BooleantruehighPerformance内部通过 Panel 的显示隐藏控制 panel 的 render 次数,避免不必要的 render,高性能模式下,只会在 Panel 展示 或者 展示隐藏状态变化时才会重新 renderBooleantrueanimationPanel 展示动画配置,内置上下左右动画

参数说明

8c7cd45898c17ec6d8ec553965e37719.png

direction 控制动画方向,分别有 updownleftrightObject

Filter 的代码使用

  • Filter 的参数配置
  navConfig: [{type: 'RelatePanel', // type可以不提供,默认值为'RelatePanel'text: '向下', // 配置筛选头文案icons: {// 配置 icon,分为正常形态和点击选中形态normal: '//gw.alicdn.com/tfs/TB1a7BSeY9YBuNjy0FgXXcxcXXa-27-30.png',active: '//gw.alicdn.com/tfs/TB1NDpme9CWBuNjy0FhXXb6EVXa-27-30.png',},hasSeperator: true, // 展示竖线分隔符formatText: text => text + '↓', // 筛选文案的格式化函数},{type: 'QuickSearch',optionsIndex: 0,optionsKey: 'price',options: [// 快速排序列表{text: '价格',icon: '',value: '0',},{text: '升序',icon: '//gw.alicdn.com/tfs/TB1PuVHXeL2gK0jSZFmXXc7iXXa-20-20.png',value: '1',},{text: '降序',icon: '//gw.alicdn.com/tfs/TB1a7BSeY9YBuNjy0FgXXcxcXXa-27-30.png',value: '2',},],},{type: 'RelatePanel', // type可以不提供,默认值为'RelatePanel'text: '旋转',icons: {// 配置 icon,分为正常形态和点击选中形态normal: '//gw.alicdn.com/tfs/TB1PuVHXeL2gK0jSZFmXXc7iXXa-20-20.png',active: '//gw.alicdn.com/tfs/TB1l4lIXhv1gK0jSZFFXXb0sXXa-20-20.png',},animation: { type: 'rotate' }, // 配置动画点击后旋转图片,默认没有动画},{type: 'RelatePanel', // type可以不提供,默认值为'RelatePanel'text: '向左',},{type: 'PureUI',text: '订阅',renderItem: () => {// 渲染自定义的 UIreturn (<Imagestyle={{width: 120,height: 92,}}source={{ uri: 'https://gw.alicdn.com/tfs/TB1eubQakL0gK0jSZFAXXcA9pXa-60-45.png' }}/>);},},]// ...<FilteroffsetTop={100} // offsetTop = RecycleView上面的组件的高度,当前为 100navConfig={this.state.navConfig} // Filter Navbar 配置项keepHighlight={true} // 保持变更的高亮styles={styles} // 配置覆盖内置样式,大样式对象集合onChange={this.handleSearchChange}// Panel 面板显示隐藏变更事件onPanelVisibleChange={this.handlePanelVisibleChange}><Panel highPerformance={true}><ListSelect {...this.state.data1} /></Panel><Panel><LocationSelect {...this.state.data2} /></Panel><PaneldisplayMode={'Fullscreen'} // 配置 Panel 全屏展示,默认为下拉展示animation={{// 动画配置timingFunction: 'cubic-bezier(0.22, 0.61, 0.36, 1)',duration: 200,direction: 'left', // 动画方向:从右往左方向滑出}}><MultiSelect {...this.state.data3} /></Panel></Filter>

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

核心源码展示

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

Filter.js

先从 render 方法看起

  render() {const { style = {}, styles = {}, navConfig, keepHighlight } = this.props;const { windowHeight, activeIndex } = this.state;if (!windowHeight) return null;return (<View style={[defaultStyle.container, styles.container, style]}>{this.renderPanels()}<Navbarref={r => {this.refNavbar = r;}}navConfig={navConfig}styles={styles}keepHighlight={keepHighlight}activeIndex={activeIndex}onNavbarPress={this.handleNavbarPress}onChange={this.handleSearchChange}/></View>);}

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

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

renderPanel 方法就是渲染对应的 panel

  /*** 渲染 Panel*/renderPanels = () => {const { activeIndex, windowHeight } = this.state;let { children } = this.props;if (!Array.isArray(children)) {children = [children];}let index = 0;return children.map(child => {let panelChild = null;let hasPanel = this.panelIndexes[index];if (!hasPanel) {index++;}if (!this.panelManager[index]) {this.panelManager[index] = {};}let injectProps = {index,visible: activeIndex === index,windowHeight,filterBarHeight: this.filterBarHeight,maxHeight: this.filterPanelMaxHeight,shouldInitialRender: this.panelManager[index].shouldInitialRender,onChange: this.handleSearchChange.bind(this, index),onNavTextChange: this.handleNavTextChange.bind(this, index),onHidePanel: this.setPanelVisible.bind(this, false, index),onMaskClick: this.handleMaskClick,disableNavbarClick: this.disableNavbarClick,};if (child.type !== Panel) {panelChild = <Panel {...injectProps}>{child}</Panel>;} else {panelChild = cloneElement(child, injectProps);}index++;return panelChild;});};

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

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

  return (<Viewref={r => {this.refPanelContainer = r;}}style={[defaultStyle.panel,styles.panel,this.panelContainerStyle,{transform: `translateX(-${this.containerTransformDes})`,opacity: 0,},]}><Viewref="mask"style={[defaultStyle.mask,styles.mask,showStyle,isWeb ? { top: 0, zIndex: -1 } : { top: 0 },]}onClick={this.handleMaskClick}onTouchMove={this.handleMaskTouchMove}/>{cloneElement(child, injectProps)}</View>);

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

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

export default function performance(Comp) {return class Performance extends Comp {static displayName = `Performance(${Comp.displayName})`;render() {const { shouldInitialRender } = this.props.panelAttributes;if (shouldInitialRender) {return super.render();} else {return <View />;}}};
}

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

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

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

NavBar 核心代码

NavBar 架构

3dac0a342d7f580bd7d09ee0f766d85d.png

核心代码

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

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

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

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

  //Filter.js 调用 NavBar 的方法/*** 更新 Navbar 文案*/handleNavTextChange = (index, navText, isChange = true) => {// Navbar 的 render 抽离到内部处理,可以减少一次 Filter.Panel 的额外 renderthis.asyncTask(() => {this.refNavbar.updateOptions(index, navText, isChange);});};//NavBar.js 提供给 Filter.js 调用的 updateOptions/*** 更新 navConfig,Filter 组件调用* 异步 setState 规避 rax 框架 bug: 用户在 componentDidMount 函数中调用中 this.props.onChange 回调* 重现Code:https://jsplayground.taobao.org/raxplayground/cefec50a-dfe5-4e77-a29a-af2bbfcfcda3* @param index* @param text* @param isChange*/updateOptions = (index, text, isChange = true) => {setTimeout(() => {const { navConfig } = this.state;this.setState({navConfig: navConfig.map((item, i) => {if (index === i) {return {...item,text,isChange,};}return item;}),});}, 0);};

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

  • NavBase 部分代码
  renderDefaultItem = ({ text, icons, active }) => {const { formatText, hasSeperator, length, keepHighlight, isChange } = this.props;const hasChange = keepHighlight && isChange;const iconWidth = icons ? this.getStyle('navIcon').width || 18 : 0;return [<TextnumberOfLines={1}style={[this.getStyle('navText'),ifElse(active || hasChange, this.getStyle('activeNavText')),{ maxWidth: 750 / length - iconWidth },]}>{ifElse(is('Function')(formatText), formatText(text), text)}</Text>,ifElse(icons,<Imageref={r => {this.refImg = r;}}style={this.getStyle('navIcon')}source={{uri: ifElse(active || hasChange, icons && icons.active, icons && icons.normal),}}/>,null,),ifElse(hasSeperator, <View style={this.navSeperatorStyle} />),];};
  • NavRelatePanel.js
  export default class NavRelatePanel extends NavBase {static displayName = 'NavRelatePanel';handleClick = () => {const { disabled, onNavbarPress } = this.props;if (disabled) return false;onNavbarPress(NAV_TYPE.RelatePanel);};render() {const { renderItem, active, text, icons } = this.props;return (<Viewstyle={[this.getStyle('navItem'), ifElse(active, this.getStyle('activeNavItem'))]}onClick={this.handleClick}>{ifElse(is('Function')(renderItem),renderItem && renderItem({ active, instance: this }),this.renderDefaultItem({ text, icons, active }),)}</View>);}}

Panel 核心代码

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

Panel 的使用:

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

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

  get animationConfig() {const { animation } = this.props;if (!animation || !is('Object')(animation)) {return PANEL_ANIMATION_CONFIG;}return Object.assign({}, PANEL_ANIMATION_CONFIG, animation);}// ... /*** 执行动画* @param nextProps*/componentWillReceiveProps(nextProps) {if (nextProps.visible !== this.props.visible) {if (nextProps.visible) {setNativeProps(findDOMNode(this.refPanelContainer), {style: {transform: `translateX(-${rem2px(750)})`,},});this.props.disableNavbarClick(true);this.enterAnimate(this.currentChildref, () => {this.props.disableNavbarClick(false);});this.handleMaskAnimate(true);} else {this.handleMaskAnimate(false);this.props.disableNavbarClick(true);this.leaveAnimate(this.currentChildref, () => {this.props.disableNavbarClick(false);setNativeProps(findDOMNode(this.refPanelContainer), {style: {transform: 'translateX(0)',},});});}}}

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

a596a20d55d384fedb58f01ed8b950a8.gif

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

      // 设置动画初始样式setTimeout(() => {setNativeProps(node, {style: {transform: !visible ? 'translate(0, 0)' : v,},});}, 0);// 执行动画setTimeout(() => {transition(node,{transform: visible ? 'translate(0, 0)' : v,},{timingFunction: timingFunction,duration: duration,delay: 0,},cb,);}, 50);

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

        setNativeProps(findDOMNode(this.refPanelContainer), {style: {opacity: 1,},});

结束语

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

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

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

技术交流

欢迎关注微信公众号:全栈前端精选,每日获取高质量文章推送。也可以加我个人微信交流~

deed9cc301fbc612d36cae1e7b31826f.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/261684.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python的正则表达式 re

2019独角兽企业重金招聘Python工程师标准>>> 原文发表在&#xff1a; http://luy.li/2010/05/12/python-re/ 延伸阅读&#xff1a;python的 内建函数 和 subprocess 。此文是本系列的第三篇文章了&#xff0c;和之前一样&#xff0c;内容出自官方文档&#xff0c;但…

福克斯保养明细

福克斯轮胎厚度&#xff1a; 胎冠厚度在13mm左右(胎冠花纹深7mm、胎冠厚6mm&#xff09;&#xff0c; 胎侧厚度5mm。 现在的轿车轮胎一般胎面胶层厚度都比较厚&#xff0c;以165/70R13 锦湖KR19轮胎为例:花纹深度为7毫米&#xff0c;缓冲层1&#xff0d;1.5毫米&#xff0c;冠带…

USACO 1.1 Your Ride Is Here

今天开始切USACO 加油 /* ID: aznfy1 PROG: ride LANG: C */ #include <iostream> #include <fstream> #include <string> #include <stdio.h>using namespace std;char a[10],b[10];int main() {freopen("ride.in","r",stdin);f…

Mysql支持的数据类型(总结)

2019独角兽企业重金招聘Python工程师标准>>> 一.数值类型 Mysql支持所有标准SQL中的数值类型&#xff0c;其中包括严格数据类型(INTEGER,SMALLINT,DECIMAL,NUMBERIC)&#xff0c;以及近似数值数据类型(FLOAT,REAL,DOUBLE PRESISION),并在此基础上进行扩展。 扩展后增…

uniapp 获取到js文件var一个变量怎么获取到这个变量值_浅析Js中const,let,var的区别及作用域...

理解&#xff1a;let变量的作用域只能在当前函数中js中const,let,var的区别及作用域_lianzhang861的博客-CSDN博客​blog.csdn.net全局作用域中&#xff0c;用 const 和 let 声明的变量不在 window 上&#xff0c;那到底在哪里&#xff1f;如何去获取&#xff1f;​blog.csdn.n…

C语言求十个数中最大值

一.代码 #define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() {int arr[] { 1,2,3,4,5,63,7,8,9,11 };int sz sizeof(arr) / sizeof(arr[0]);int max arr[0];int i 0;for (i 1; i < 10; i){if (arr[i] > max)max arr[i];}printf("%d\n&qu…

ubuntu20.04安装timeshift最新方法

总结&#xff1a; 现在可以使用如下代码安装 sudo apt-get update sudo apt-get install timeshift原因&#xff1a; 在尝试Timeshift系统备份与还原中的方法时&#xff0c; sudo apt-add-repository -y ppa:teejee2008/ppa运行失败。 更改为以下代码&#xff1a; sudo a…

SQL Server安全(6/11):执行上下文与代码签名(Execution Context and Code Signing)

在保密你的服务器和数据&#xff0c;防备当前复杂的攻击&#xff0c;SQL Server有你需要的一切。但在你能有效使用这些安全功能前&#xff0c;你需要理解你面对的威胁和一些基本的安全概念。这篇文章提供了基础&#xff0c;因此你可以对SQL Server里的安全功能充分利用&#xf…

索引超出数组界限是什么意思_从V8源码分析一个JS 数组的内存占用问题

前段时间&#xff0c;在排查一个问题的时候&#xff0c;遇到了一个有点令人困惑的情况&#xff0c;有下面这两段代码&#xff1a;const a new Array(99999); a[99998] undefined;const b new Array(99999); b[99999] undefined;我们通过 node --inspect-brk 来分别运行这两…

C语言打印九九乘法口诀

一.代码 #define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() {int i 0;int j 0;for (i 1; i < 10; i){for (j 1; j < i; j){printf("%d*%d%-2d", i, j, i * j);}printf("\n");}return 0; }二.运行结果

自动机理论、形式语言和计算导论提纲

我真的是觉得这门课太虚了。。这个总结基于名教材《自动机理论、语言和计算导论》&#xff08;机械工业&#xff09;&#xff0c;也可以说是这本书的总结。由于这门课里很多罗马字母&#xff0c;打字很困难所以能省略的公式都不写了&#xff0c;可以算是入门介绍了。这里省略的…

C语言求斐波那契数列

一.递归算法 #define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int fib(int k) {if (k < 2)return 1;elsereturn fib(k - 1) fib(k - 2); } int main() {int n 0;int res 0;printf("求输入斐波那契数列的阶数");scanf("%d", &n);res…

nginx $mail-send()发送邮件报错_基于SMTP协议的E-MAIL电子邮件发送客户端软件C#实现...

摘 要电子邮件在当今社会中扮演了一个很重要的角色。越来越多的人在使用它。而且用它的人数势必会继续增加。虽然&#xff0c;现在已经有很多的邮件收发软件例如著名的FoxMail 但是对于大多数的非专业的人来说它还是有点难度稍嫌负责。因此&#xff0c;我们就利用SMTP和Pop协议…

【转】DCT变换的透彻解析

3、离散余弦变换 DCT  将图像从色彩域转换到频率域&#xff0c;常用的变换方法有&#xff1a;DCT变换的公式为&#xff1a;f(i&#xff0c;j) 经 DCT 变换之后&#xff0c;F(0&#xff0c;0) 是直流系数&#xff0c;其他为交流系数。  还是举例来说明一下。  8x8的原始图…

安防硬件WIZnet基于全硬件TCP/IP的安防产品应用及方案

在本文中,我们主要介绍安防硬件的内容,自我感觉有个不错的建议和大家分享下 WIZnet立足于生产全硬件TCP/IP协议栈芯片&#xff0c;为单片机提供理想的处置计划。整体来讲&#xff0c;其应用领域还是非常广泛的&#xff0c;以智能电表为代表的Smart Energy&#xff1b;以数字楼宇…

在Android命令行启动程序的方法

在Android中&#xff0c;除了从界面上启动程序之外&#xff0c;还可以从命令行启动程序&#xff0c;使用的是命令行工具am.启动的方法为 # am start -n 包(package)名/包名.活动(activity)名称 启动的方法可以从每个应用的AndroidManifest.xml的文件中得到&#xff0c;以计算器…

python每隔半个小时执行一次_一篇文章教你用Python抓取微博评论

【Part1——理论篇】试想一个问题&#xff0c;如果我们要抓取某个微博大V微博的评论数据&#xff0c;应该怎么实现呢&#xff1f;最简单的做法就是找到微博评论数据接口&#xff0c;然后通过改变参数来获取最新数据并保存。首先从微博api寻找抓取评论的接口&#xff0c;如下图所…

r数据框计算字符出现次数_R语言系列第二期:①R变量、脚本、作图等模块介绍...

在上一篇文章里&#xff0c;给大家介绍了R语言的下载&#xff0c;界面操作&#xff0c;6个处理对象等等。在这些内容的基础上&#xff0c;我们在这个部分为大家介绍一些实用知识&#xff0c;包括描述工作区结构、图形设备以及它们的参数等问题&#xff0c;还有初级编程和数据输…

$.AjaxFileUpload is not a function

2019独角兽企业重金招聘Python工程师标准>>> ..is not a function错误的可能情况&#xff1a; 1、JS引入的路径不对。检查方法是看浏览器控制台是否将JS载入了进来。 2、JS引入顺序不对。JS要在你使用之前引入 3、Jquery没有第一个引入。 4、函数所在script标签&…

代理对象我所理解的设计模式(C++实现)——代理模式(Proxy Pattern)

文章结束给大家来个程序员笑话&#xff1a;[M] 概述 作为C工程师&#xff0c;免不了要管理内存&#xff0c;内存管理也是C中的难点&#xff0c;而智能指针采用引用计数的方法很方便的帮我们管理了内存的应用&#xff0c;极大方便了我们的任务效率。而智能指针的这类用法其实就是…