We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
此文前端框架使用 rax,全篇代码暂未开源(待开源)
貌似在面试中,你如果设计一个 react/vue 组件,貌似已经是司空见惯的问题了。本文不是理论片,更多的是自己的一步步思考和实践。文中会有很多笔者的思考过程,欢迎评论区多多交流和讨论。
从需求讨论、技术方案探讨到编码、到最终的测试,经历过了很多次的脑暴,也遇到过非常多的坑,其中有可能跟业务有关、也有可能跟框架有关,基于这些坑,又讨论了很多解决方案和非常 hack(歪门邪道)的对策。但是随着时间的推移,再回头看看当时的 hack 代码,很多都不太记得为什么这么写了,所以这里简单记录下,Filter 组件的开发过程。以便后面查询,更希望能大家一起探讨,以求得更优质的代码架构和实现思路。
由于代码编写使用基于底层 weex 的 rax 框架,所以有些坑,或许对于正在使用 react 或者 vue 的你并不会遇到,可以直接忽略
Filter,已经常见的不可再常见的组件了,顾名思义,就是个筛选过滤器。我们先看看现有 app 上的一些 filter 展现 形式。既然做组件,我们就需要它足够的通用,足够的易于扩展。
在说 Filter 的业务特征之前,我们先约束下每一部分的命名,以便于你更好的阅读此文:
上面分别是拍卖和飞猪的 filter 页面,从这两个页面中,我们大概可以总结出关于 Filter 的一下几点业务画像:
由于 rax 1.0 ts+hooks 开源版本还在开发中,所以仓库链接暂时就不放上了
效果图:
console 处可见抛出的查询参数
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
RelatePanel
QuickSearch
PureUI
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 却不送呢?所以到底是不是标配?
对于我们这个组件,简而言之:我们能做到的,我们都做!但是其中我们还是梳理出某些功能还是数据业务功能:
换言之,Filter 里面任何功能都可以说为业务功能。但是我们需要提供 80%业务都需要的功能封装作为 Filter 的 Future。这就是我们的目的。
根据上面的业务功能和组件功能的区分,我们就知道在使用 Filter 的时候,你应该给我传递什么配置,以及什么方法。
styles
pm-app-plus
Function(params:Object,index:Number, urlQuery: Object) => void
params: Object
index:Number
urlQuery:Object
Function({ visible:Boolean, triggerIndex:Number, triggerType:String }) => void
visible:Boolean
triggerIndex:Number
triggerType:String
Navbar
Mask
Panel
筛选项类型 type
注意 如果 navConfig 内置的UI参数不满足您的需求,请使用renderItem自定义渲染函数来控制筛选头 UI
renderItem
...
Object
String
Function(text:String) => text
text: String
Function(isActive:Boolean, this:Element) => Element
isActive:Boolean
this:Element
rotate
Function(refImg:Element, isActive:Boolean) => text
refImg:Element
Fullscreen
Dropdown
direction
up
down
left
right
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 的版本编写的代码。这里只做,有坑的地方代码处理讲解。欢迎各位大佬评论留出各位想法
先从 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 下面出来的。
renderPanels
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
transform
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。
locationId=123
ref
所以最终,为了提高页面的可交互率但是又不影响页面需求的情况下,我们提供了一个可选的工具: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 中通过不同的配置,展示不同的 NavBarItem 的类型,NavQuickSearch,NavRelatePanel
NavQuickSearch
NavRelatePanel
这里需要注意的是: NavBar 的数据是通过 Filter props 传入的,如果状态放到 Filter 也就是 NavBar 的父组件管理的话,会导致 Panel 组件不必要的渲染(虽然已经提供 Panel 层的 shouldComponentUpdate 的配置参数),同时也是为了组件设计的高内聚、低耦合,我们将传入的 props 封装到 NavBar 的 state 中,自己管理状态。
Filter
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} />), ]; };
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.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 还是闪了一下,只是你看不到而已。
opacity
// 设置动画初始样式 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业务需求(坑)你有更好的处理方法和想法都欢迎在评论区交流~
欢迎关注微信公众号:全栈前端精选,每日获取高质量文章推送。也可以加我个人微信交流~
The text was updated successfully, but these errors were encountered:
No branches or pull requests
前言
貌似在面试中,你如果设计一个 react/vue 组件,貌似已经是司空见惯的问题了。本文不是理论片,更多的是自己的一步步思考和实践。文中会有很多笔者的思考过程,欢迎评论区多多交流和讨论。
从需求讨论、技术方案探讨到编码、到最终的测试,经历过了很多次的脑暴,也遇到过非常多的坑,其中有可能跟业务有关、也有可能跟框架有关,基于这些坑,又讨论了很多解决方案和非常 hack(歪门邪道)的对策。但是随着时间的推移,再回头看看当时的 hack 代码,很多都不太记得为什么这么写了,所以这里简单记录下,Filter 组件的开发过程。以便后面查询,更希望能大家一起探讨,以求得更优质的代码架构和实现思路。
说说业务
Filter,已经常见的不可再常见的组件了,顾名思义,就是个筛选过滤器。我们先看看现有 app 上的一些 filter 展现 形式。既然做组件,我们就需要它足够的通用,足够的易于扩展。
在说 Filter 的业务特征之前,我们先约束下每一部分的命名,以便于你更好的阅读此文:
上面分别是拍卖和飞猪的 filter 页面,从这两个页面中,我们大概可以总结出关于 Filter 的一下几点业务画像:
最终组件产出
效果图:
console
处可见抛出的查询参数设计与思考
前端组件架构图(初版)
组件架构图(终板)
组件功能 Feature
RelatePanel
:筛选项关联Panel型,即筛选头和 Panel 是一对一关系,点击筛选头展示 PanelQuickSearch
:筛选项快速搜索排序型,即筛选头没有对应 Panel,点击筛选头直接触发搜索PureUI
:纯 UI占位类型,即纯 UI 放置,不涉及搜索,比如订阅按钮场景onChange(params)
回调函数来触发rax-pui-list-select
,列表选择业务面板rax-pui-location-select
,省市区级联选择业务面板rax-pui-multi-selection-panel
,多选业务面板,查看组件使用文档期望组件使用形式
组件功能与业务需求边界划分
何为业务功能何为组件功能,这个需要具体的探讨,其实也没有严格意义上的区分。说白了,就是你买个手机,他都会送你充电器。但是。。。为什么很多手机也送手机壳(小米、华为、荣耀)但是 iPhone 却不送呢?所以到底是不是标配?
对于我们这个组件,简而言之:我们能做到的,我们都做!但是其中我们还是梳理出某些功能还是数据业务功能:
换言之,Filter 里面任何功能都可以说为业务功能。但是我们需要提供 80%业务都需要的功能封装作为 Filter 的 Future。这就是我们的目的。
根据上面的业务功能和组件功能的区分,我们就知道在使用 Filter 的时候,你应该给我传递什么配置,以及什么方法。
Filter API
效果图
固定位置 状态下距离页面顶部的高度
跟随页面滚动吸附置顶: 状态下距离页面顶部的高度
效果图
styles
集合对象来配置覆盖styles 格式
pm-app-plus
容器组件点击 Filter 时自动吸附置顶示例图
效果图
签名:
Function(params:Object,index:Number, urlQuery: Object) => void
参数:
params: Object
搜索参数index:Number
触发搜索的 Panel 搜索urlQuery:Object
URL query 对象签名:
Function({ visible:Boolean, triggerIndex:Number, triggerType:String }) => void
参数:
visible:Boolean
显示隐藏标志量triggerIndex:Number
触发的筛选项索引值triggerType:String
触发类型triggerType详解 包含三种触发类型
Navbar
:来自筛选头的点击触发Mask
:来自背景层的点击触发Panel
:来自Panel 的 onChange 回调触发Filter prop navConfig 数组配置详解
navConfig
筛选项类型 type
RelatePanel
:筛选项关联Panel型,即筛选头和 Panel 是一对一关系,点击筛选头展示 PanelQuickSearch
:筛选项快速搜索排序型,即筛选头没有对应 Panel,点击筛选头直接触发搜索PureUI
:纯 UI占位类型,即纯 UI 放置,不涉及搜索,比如订阅按钮场景注意 如果 navConfig 内置的UI参数不满足您的需求,请使用
renderItem
自定义渲染函数来控制筛选头 UI三种类型
RelatePanel
: 筛选项关联数据面板类型QuickSearch
: 筛选项快速搜索排序类型PureUI
: 纯 UI占位类型注意
RelatePanel
类型生效文字溢出用
...
展示注意
RelatePanel
类型生效数据格式
Object
类型 :String
类型 :效果图
注意
QuickSearch
类型生效数据格式
注意
QuickSearch
类型生效注意
QuickSearch
类型生效签名:
Function(text:String) => text
参数:
text: String
筛选头文案效果图
注意
提供的配置项无法满足你的 UI 需求时使用
签名:
Function(isActive:Boolean, this:Element) => Element
参数:
isActive:Boolean
筛选头是否为激活状态this:Element
筛选头this实例参数说明
注意 目前只内置了一种
rotate
动画类型签名:
Function(refImg:Element, isActive:Boolean) => text
参数:
refImg:Element
筛选头图标的 ref 实例isActive:Boolean
筛选头是否为激活状态Filter.Panel API
Filter中所有样式都可使用
styles
集合对象来配置覆盖参数说明
全屏:
Fullscreen
下拉:
Dropdown
参数说明
direction
控制动画方向,分别有up
、down
、left
、right
Filter 的代码使用
代码运行效果图如上截图。下面,简单说下代码的实现。
核心源码展示
Filter.js
先从 render 方法看起
获取一些基本配置,以及 windowHeight(屏幕高度)和 activeIndex(当前第几个item 处于 active 状态(被点开))。
之所以我们的
renderPanels
写在NavBar
上面,是因为在 weex 中,zIndex 是不生效的。若想 A 元素在 B 元素上面,则 render 的时候,A 必须在 B 后面。这样写是为了 panel 面板展开的下拉动画,看起来是从 navBar 下面出来的。renderPanel 方法就是渲染对应的 panel
准确的说,这是一个 HOC,我们将代理、翻译传给 Filter 的影响或者 panel 面板需要使用的 props 传递给 Panel 面板。比如 onChange 回调,或者面板隐藏的回调以及当前哪一个 panel 需要展开等。
由于 Panel 的面板复杂度我们未知。为了避免不断的展开和收齐不必要的 render,我们采用
transform
的方式,将面板不需要显示的面板移除屏幕外,需要展示的在移入到屏幕内部。具体可见 Panel 的render return注意: Panel 面板的坑远不止这些,比如,我们都知道,render 是最消耗页面性能的,而页面初始化进来,面板名没有展示出来(此时面板 Panel 在屏幕外),那么是否需要走 Panel 面板的 render 呢?但是目前的这种写法,Panel 组件的生命周期是会都走到的。但是如果遇到 Panel 里面需要请求数据,然后页面 url 里查询参数有
locationId=123
,navItem 需要展示对应的地理位置.如果不渲染 Panel 如何根据 id 拿到对应的地名传递给 navItem 去展示?对,我们可以拦截 Panel 面板的 render 方法,让 Panel render null,然后别的生命周期照样运行。但是,如果 render 中用户有对ref
的使用,那么就可能会造成难以排查的 bug。所以最终,为了提高页面的可交互率但是又不影响页面需求的情况下,我们提供了一个可选的工具:Performance HOC 。 注意,是可选。
通过配置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 中,自己管理状态。NavBar 中还需要注意的就是被动更新:Panel 层点击后,NavBar 上文字的更新,因为这里我们利用父组件来进行 Panel 和 NavBar 的通信
最后 NavBar 中的 item 分为 快速搜索和带有 panel 的 NavBarItem两种,但是对于其公共功能,比如渲染的 UI 逻辑等,这里我们采用的方法是抽离
NavBase
组件,供给NavQuickSearch
和NavRelatePanel
调用:Panel 核心代码
Panel 的核心功能是对用户定义的 Panel.child 进行基本的功能添加,比如背景 mask 遮罩、动画时机的处理.
Panel 的使用:
我们提供基础的动画配置,但是同时,也提供动画的 functionHook,这些都取决于动画的触发时机
由于动画的执行需要时间,所以这个时间段,我们应该给 Filter 中的 NavBar 加锁 ,锁的概念也同样提供给用户,毕竟业务逻辑我们是不会侵入的,在上一次的搜索没有结果返回时候,应该给 NavBar 加锁,禁止再次点击(虽然用户可以再 onchange 回调函数中处理,但是作为组件,同样应该考虑并且提供这个能力),同样对于动画也是如此,在该动画正在执行的时候,应该禁止 NavBar 的再次点击。上面的动画配置效果如下:
Panel 中还有核心的处理或许就是关于动画时机的处理。比如在触发动画前,我们需要设置动画初始状态,但是如若如下写法,会出现 Panel 闪动的现象,毕竟我们通过第二次的事件轮训回来才执行初始化,所以这里,如果用户配置启动动画,那么我们需要在 Panel 的最外层添加一个可见的 flag:默认进来
opacity
设置为 0,当动画初始状态设置完毕后,在将最外层容器的opacity
设置为 1,其实 Panel 还是闪了一下,只是你看不到而已。设置动画初始化样式中添加:
结束语
Filter 的组件看似简单,但是如果想写一个市场上较为通用和广泛的 Filter 组件,不仅仅是组件的颗粒度、耦合度和性能需要考虑,更多的是其中还是有太多的业务逻辑需要去思考。对于目前的初版(还未修改成正式开源版),已经基本涵盖了目前我们能够想到的业务场景,也已经有相关业务落地使用。
当然,对于如果是直接放到业务中使用而不作为开源组件的话,我们可已经 Panel下的 child 通过 renderPortal 降低层级,通过 EventBus 或者 redux、mobx 等管理数据状态。那样会让整个代码逻辑看起来清晰很多。但是为了降低bundle 大小,我们尽可能的减少通用包的使用以及第三方插件的依赖。
关于文章中没有提及的想法或者对于这些Filter业务需求(坑)你有更好的处理方法和想法都欢迎在评论区交流~
技术交流
The text was updated successfully, but these errors were encountered: