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

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

前言

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

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

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

说说业务

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

  • 阿里拍卖的 Filter

paimai

  • 飞猪的 Filter

feizhu

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

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 开源版本还在开发中,因此仓库连接暂时就不放上了docker

  • rax-pui-filter-utils : Filter 的内部工具库,仅供 Filter 开发者提供的工具库
  • rax-pui-filter-tools:配合使用 Filter 的一些工具集,好比 提升性能的 HOC 组件、占位符组件等(可用可不用,根据本身业务需求来),思考起因:并非每个 Filter 的使用者都须要这些功能,作成可插拔式,为了下降不必的 bundle 大小
  • pui-filter:Filter 核心功能开发库

效果图:redux

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 筛选头配置, <a href='#navConfig'>点击查看详细配置项 </a> <br /><br />效果图<br /> undefined Array<Object> - (必填)
offsetTop Filter组件展开面板状态下距离页面顶部的高度,有两种状态:固定位置跟随页面滚动吸附置顶<br /><br /> 固定位置 状态下距离页面顶部的高度<br /> 跟随页面滚动吸附置顶: 状态下距离页面顶部的高度 <br /><br />效果图<br /> undefined Number 0
styles 配置样式,Filter中全部样式均可使用styles集合对象来配置覆盖<br />styles 格式<br />undefined Object {}
getStickyRef 获取 Sticky 节点的 ref 实例,用于滚动吸附场景,内部配合 pm-app-plus 容器组件点击 Filter 时自动吸附置顶<br /><br />示例图<br />undefined Function
keepHighlight 筛选条件改变后是否须要在筛选头保持高亮<br /><br />效果图<br />undefined Boolean false
clickMaskClosable 开启 mask 背景的点击隐藏 Boolean true
onChange Filter 搜索变动回调函数 <br /> 签名: Function(params:Object,index:Number, urlQuery: Object) => void <br /> 参数: <br />params: Object 搜索参数<br />index:Number 触发搜索的 Panel 搜索<br />urlQuery:Object URL query 对象<br /> Function
onPanelVisibleChange Panel 显示隐藏回调函数 <br /> 签名: Function({ visible:Boolean, triggerIndex:Number, triggerType:String }) => void <br /> 参数: <br /> visible:Boolean 显示隐藏标志量 <br /> triggerIndex:Number触发的筛选项索引值 <br />triggerType:String 触发类型 <br /> <br /><br />triggerType详解 包含三种触发类型<br />Navbar:来自筛选头的点击触发<br />Mask:来自背景层的点击触发<br />Panel:来自Panel 的 onChange 回调触发 Function

Filter prop navConfig 数组配置详解

navConfig

筛选项类型 type

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

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

参数 说明 类型 默认值(是否必填)
type 筛选项类型 <br /><br /> 三种类型<br/>RelatePanel: 筛选项关联数据面板类型<br/>QuickSearch: 筛选项快速搜索排序类型<br/>PureUI: 纯 UI占位类型 String 'RelatePanel'
text <br /><br /><br /> 注意 RelatePanel类型生效 筛选头显示文案 <br /> 文字溢出用...展现 String - (必填)
icons <br /><br /><br /> 注意 RelatePanel类型生效 筛选头 icon:normal 正常态 和 active 激活态 图标 <br /> 数据格式 <br /> Object类型 :<br /> undefined <br /> String类型 : <br />undefined <br /><br />效果图<br /> undefined Object or String -
options <br /><br /><br /> 注意 QuickSearch类型生效 快速搜索排序类型的数据源 <br />数据格式<br /> undefined Array (必填)
optionsIndex <br /><br /><br /> 注意 QuickSearch类型生效 快速搜索排序类型默认选中的索引 String 0
optionsKey <br /><br /><br /> 注意 QuickSearch类型生效 指定快速搜索排序对应的搜索 key,用到 onChange 回调中 String 不提供默认使用当前筛选项的索引
formatText 文案格式化函数<br /> 签名:Function(text:String) => text <br /> 参数: <br />text: String 筛选头文案 Function (text)=>text
disabled 禁用筛选头点击 Boolean true
hasSeperator 是否展现右侧分隔符<br /><br />效果图<br /> undefined Boolean false
hasPanel 当前筛选头是否有对应的 panel Boolean true
renderItem 自定义渲染<br /> 注意 <br /> 提供的配置项没法知足你的 UI 需求时使用<br /> 签名:Function(isActive:Boolean, this:Element) => Element <br /> 参数: <br />isActive:Boolean 筛选头是否为激活状态<br />this:Element 筛选头this实例 Function -
animation 动画配置,采用内置的动画 <br />参数说明 <br />undefined <br /> 注意 目前只内置了一种rotate动画类型 Object
animationHook 用户自定义动画的钩子函数,内置动画没法知足需求时使用 <br /> 签名:Function(refImg:Element, isActive:Boolean) => text <br /> 参数: <br />refImg:Element 筛选头图标的 ref 实例 <br />isActive:Boolean 筛选头是否为激活状态 Function -

Filter.Panel API

参数 说明 类型 默认值(是否必填)
styles 配置样式<br />Filter中全部样式均可使用styles集合对象来配置覆盖 Object {}
displayMode Panel 展示形式:全屏、下拉 <br />参数说明 <br />全屏:Fullscreen<br />下拉:Dropdown String 'Dropdown'
noAnimation 禁止动画 Boolean true
highPerformance 内部经过 Panel 的显示隐藏控制 panel 的 render 次数,避免没必要要的 render,高性能模式下,只会在 Panel 展现 或者 展现隐藏状态变化时才会从新 render Boolean true
animation Panel 展现动画配置,内置上下左右动画 <br />参数说明<br /> undefined <br /> direction 控制动画方向,分别有 updownleftright 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 组件,供给 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 [
      <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业务需求(坑)你有更好的处理方法和想法都欢迎在评论区交流~

学习交流

关注公众号: 【全栈前端精选】 每日获取好文推荐。还能够入群,一块儿学习交流呀~~

相关文章
相关标签/搜索