记一个复杂组件(Filter)的从设计到开发
此文前端框架使用 ,全篇代码暂未开源(待开源)
原文链接地址:nealyang/personalblog
前言
貌似在面试中,你如果设计一个 react/vue 组件,貌似已经是司空见惯的问题了。本文不是理论片,更多的是自己的一步步思考和实践。文中会有很多笔者的思考过程,欢迎评论区多多交流和讨论。
从需求讨论、技术方案探讨到编码、到最终的测试,经历过了很多次的脑暴,也遇到过非常多的坑,其中有可能跟业务有关、也有可能跟框架有关,基于这些坑,又讨论了很多解决方案和非常 hack(歪门邪道)的对策。但是随着时间的推移,再回头看看当时的 hack 代码,很多都不太记得为什么这么写了,所以这里简单记录下,filter 组件的开发过程。以便后面查询,更希望能大家一起探讨,以求得更优质的代码架构和实现思路。
由于代码编写使用基于底层 weex 的 rax 框架,所以有些坑,或许对于正在使用 react 或者 vue 的你并不会遇到,可以直接忽略
说说业务
filter,已经常见的不可再常见的组件了,顾名思义,就是个筛选过滤器。我们先看看现有 app 上的一些 filter 展现 形式。既然做组件,我们就需要它足够的通用,足够的易于扩展。
- 阿里拍卖的 filter
- 飞猪的 filter
在说 filter 的业务特征之前,我们先约束下每一部分的命名,以便于你更好的阅读此文:
上面分别是拍卖和飞猪的 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
处可见抛出的查询参数
设计与思考
前端组件架构图(初版)
组件架构图(终板)
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( <filter navconfig={[]} 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 | 筛选头配置, 点击查看详细配置项 效果图 |
array<object> | - (必填) |
offsettop | filter组件展开面板状态下距离页面顶部的高度,有两种状态:固定位置和跟随页面滚动吸附置顶 固定位置 状态下距离页面顶部的高度 跟随页面滚动吸附置顶: 状态下距离页面顶部的高度 效果图 |
number | 0 |
styles | 配置样式,filter中所有样式都可使用styles 集合对象来配置覆盖styles 格式 |
object | {} |
getstickyref | 获取 sticky 节点的 ref 实例,用于滚动吸附场景,内部配合 pm-app-plus 容器组件点击 filter 时自动吸附置顶示例图 |
function | |
keephighlight | 筛选条件改变后是否需要在筛选头保持高亮 效果图 |
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 数组配置详解
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 类型 :string 类型 : 效果图 |
object or string | - |
options 注意 quicksearch 类型生效 |
快速搜索排序类型的数据源 数据格式 |
array | (必填) |
optionsindex 注意 quicksearch 类型生效 |
快速搜索排序类型默认选中的索引 | string | 0 |
optionskey 注意 quicksearch 类型生效 |
指定快速搜索排序对应的搜索 key,用到 onchange 回调中 | string | 不提供默认使用当前筛选项的索引 |
formattext | 文案格式化函数 签名: function(text:string) => text 参数: text: string 筛选头文案 |
function | (text)=>text |
disabled | 禁用筛选头点击 | boolean | true |
hasseperator | 是否展示右侧分隔符 效果图 |
boolean | false |
haspanel | 当前筛选头是否有对应的 panel | boolean | true |
renderitem | 自定义渲染 注意 提供的配置项无法满足你的 ui 需求时使用 签名: function(isactive:boolean, this:element) => element 参数: isactive:boolean 筛选头是否为激活状态this:element 筛选头this实例 |
function | - |
animation | 动画配置,采用内置的动画 参数说明 注意 目前只内置了一种 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 展示动画配置,内置上下左右动画 参数说明 direction 控制动画方向,分别有 up 、down 、left 、right
|
object |
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: () => { // 渲染自定义的 ui return ( <image style={{ width: 120, height: 92, }} source={{ uri: 'https://gw.alicdn.com/tfs/tb1eubqakl0gk0jszfaxxca9pxa-60-45.png' }} /> ); }, }, ] // ... <filter offsettop={100} // offsettop = recycleview上面的组件的高度,当前为 100 navconfig={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> <panel displaymode={'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()} <navbar ref={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 ( <view ref={r => { this.refpanelcontainer = r; }} style={[ defaultstyle.panel, styles.panel, this.panelcontainerstyle, { transform: `translatex(-${this.containertransformdes})`, opacity: 0, }, ]}> <view ref="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 架构
核心代码
从架构图中大概可以看出,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 的额外 render this.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
组件,供给 navquicksearch
和 navrelatepanel
调用:
- 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 [ <text numberoflines={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, <image ref={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 ( <view style={[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 的使用:
<panel displaymode={'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 的再次点击。上面的动画配置效果如下:
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业务需求(坑)你有更好的处理方法和想法都欢迎在评论区交流~
学习交流
关注公众号: 【全栈前端精选】 每日获取好文推荐。
公众号内回复 【1】,加入全栈前端学习群,一起交流。
上一篇: 超级计算机500强史上首次全部千万亿次!中国神威太湖之光第三
下一篇: 前端实用工具大集合