記一個複雜元件(Filter)的從設計到開發
此文前端框架使用 rax,全篇程式碼暫未開源(待開源) 原文連結地址: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 是一對一關係,點選篩選頭展示 PanelQuickSearch
:篩選項快速搜尋排序型,即篩選頭沒有對應 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 /> | Array<Object> | - (必填) |
offsetTop | Filter元件展開面板狀態下距離頁面頂部的高度,有兩種狀態:固定位置和跟隨頁面滾動吸附置頂<br /><br /> 固定位置 狀態下距離頁面頂部的高度<br /> 跟隨頁面滾動吸附置頂: 狀態下距離頁面頂部的高度 <br /><br />效果圖<br /> | Number | 0 |
styles | 配置樣式,Filter中所有樣式都可使用styles 集合物件來配置覆蓋<br />styles 格式<br /> |
Object | {} |
getStickyRef | 獲取 Sticky 節點的 ref 例項,用於滾動吸附場景,內部配合 pm-app-plus 容器元件點選 Filter 時自動吸附置頂<br /><br />示例圖<br /> |
Function | |
keepHighlight | 篩選條件改變後是否需要在篩選頭保持高亮<br /><br />效果圖<br /> | 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 是一對一關係,點選篩選頭展示 PanelQuickSearch
:篩選項快速搜尋排序型,即篩選頭沒有對應 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 /> <br /> String 型別 : <br /> <br /><br />效果圖<br /> |
Object or String | - |
options <br /><br /><br /> 注意 QuickSearch 型別生效 |
快速搜尋排序型別的資料來源 <br />資料格式<br /> | 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 /> | 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 /> <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 /> <br /> 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業務需求(坑)你有更好的處理方法和想法都歡迎在評論區交流~
學習交流
關注公眾號: 【全棧前端精選】 每日獲取好文推薦。還可以入群,一起學習交流呀~~
相關推薦
記一個複雜元件(Filter)的從設計到開發
此文前端框架使用 rax,全篇程式碼暫未開源(待開源) 原文連結地址:Nealyang/PersonalBlog 前言 貌似在
編碼實現將一個文字檔案(圖片)從一個地方複製到另一個地方(源路徑到目的路徑可以通過方法引數傳入)
public class MyTest { public static void main(String[] args) { try { test(); } catch (Exception e) { e.printStackTrace(); } } publi
給定一個實數陣列,按序排列(從小到大),從陣列從找出若干個數,使得這若干個數的和與M最為接近,描述一個演算法,並給出演算法的複雜度。
有N個正實數(注意是實數,大小升序排列) x1 , x2 ... xN,另有一個實數M。 需要選出若干個x,使這幾個x的和與 M 最接近。 請描述實現演算法,並指出演算法複雜度。 #define M
洗禮靈魂,修煉python(3)--從一個簡單的print代碼揭露編碼問題,運行原理和語法習慣
比較 編譯 windows 機器 函數 容易 打印字符 出現 無法 前期工作已經準備好後,可以打開IDE編輯器了,你可以選擇python自帶的IDLE,也可以選擇第三方的,這裏我使用pycharm——一個專門為python而生的編譯器 第一個python代碼當然是所有開發語
ceph分布式存儲實戰(2)——從0開始創建第一個ceph集群
moni name exceptio swap nor 都是 -c 監視 defined 一、在每臺節點的/etc/hosts文件中增加如下內容 192.168.89.101 ceph-node1 192.168.89.102 ceph-node2 192.168.89.1
從零打造一個CMDB(一)資料庫設計
俠義的CMDB都是偏向純資產管理,但運維繫統往往圍繞著這些資產中心,從資產進行不斷外充擴容 在其基礎之外擴展出各功能,通過cmdb 擴展出各個子系統 涉及工具:workbench 一個例子:設計一個數據庫實現主機資訊、交換機資訊,如何將之間的資訊關聯起來 初步的傳統設計:
造輪子:搭建一個簡單的nodejs伺服器,從零開始搭建一個自用網站(0)
伺服器用的是阿里雲最早期的伺服器低配版本1Gcpu,512M記憶體,20G硬碟,1M頻寬,平常只是用來做測試,目前只處理業務邏輯,網站的設計上儘量避免佔用太多的頻寬, 靜態檔案的儲存用的是阿里雲oss,100G空間,夠放視訊,圖片什麼的, html/js/c
單鏈表相關(1)從尾到頭列印連結串列、刪除一個無頭連結串列的非尾結點
1、從尾到頭列印連結串列 2、刪除一個無頭連結串列的非尾結點 單鏈表結構以及Find函式參見 2016-1-2 13:56 發表部落格 void Print_T_to_H(SListNode*&am
打造一個企業級應用的微服務開發框架(上)---從服務註冊中心到服務管理中心
【摘要】近年來越來越多的企業開始實踐微服務,本文分為上下兩篇介紹微服務框架ServiceComb如何幫助企業應用進行微服務化,實現快速交付,並可靠地執行在雲端。上篇介紹ServiceComb的服務管理中心設計。近年來越來越多的企業開始實踐微服務,而微服務在企業應用落地的過程,面臨著微服務開發框架的選型,無論是
tensorflow入門(一)---從一個小程式說起
我們現在需要擬合一條直線,通常我們會選用最小二乘的方法,通過程式設計優化實現,現在我們先用tensorflow提供的API來實現這個程式。麻雀雖小五臟俱全,裡面會涉及到很多tensorflow的核心API和思想: 我們隨機生成100個點,然後對這個點進行擬合,通過梯度下降演
NDK-JNI實戰教程(三) 從比Hello World稍複雜點兒的NDK例子說說模板
PS一句:最終還是選擇CSDN來整理髮表這幾年的知識點,該文章平行遷移到CSDN。因為CSDN也支援MarkDown語法了,牛逼啊! 第一部分 概述 學習JNI NDK你需要有java與C或者C++基礎。因為NDK幾乎就是java與C或者C++
(MySQL)從簡單例子到複雜例子認識MySQL
本文將簡單介紹MySQL和彙集一些常見例子來幫大家理解sql語句,可以當做字典檢視。(前排提示,因為我是先在onenote做一次筆記,在來發blog的,但從onenote複製的內容會變成圖片,所以可能畫面有點奇怪,不過不影響學習哈哈)提綱:資料庫簡介0.MySQL使用1.資料
基礎設計模式-03 從過濾器(Filter)校驗鏈學習職責鏈模式
# 1.職責鏈路模式 ## 1.1UML圖 ## 1.2 職責鏈路模式的概念 為了解耦的任務校驗,將校驗物件連成一個鏈,沿著這個鏈進行訪問,直到有一個物件處理位置; ## 1.3 優點 1.按照一定的順序執行判斷; 2.避免校驗物件之間耦合關係; 3.不用擔心沒有程式碼沒有執行
第一個AngularJS Demo(購物車)
mod 購物 set sco tle ng-model -m -c 刪除 <!DOCTYPE html><html><head> <meta charset="utf-8" /> <script sr
SpringMVC的攔截器(Interceptor)和過濾器(Filter)的區別與聯系
get err 實例 分享 切面 簡介 () lee XML 一 簡介 (1)過濾器: 依賴於servlet容器。在實現上基於函數回調,可以對幾乎所有請求進行過濾,但是缺點是一個過濾器實例只能在容器初始化時調用一次。使用過濾器的目的是用來做一些過濾操作,獲取我們想要獲取
(轉)從Python的0.1輸出0.1000000000000001說浮點數的二進制
python2 comment 科學 交換 tps alt 三種 一段 fill 原文地址:http://blog.csdn.net/u012843100/article/details/60885763 今天在學習Python核心編程的時候,十進制浮點數那段看到一個有趣的
每天一個linux命令(16):tail命令
nvi 系統 strong 維基百科 ron .com linux tro 聯系 版權聲明更新:2017-05-20博主:LuckyAlan聯系:[email protected]/* */聲明:吃水不忘挖井人,轉載請註明出處! 1 文章介紹 本文介紹了Linu
每天一個linux命令(11):cat命令
部分 ron linu mv命令 平臺 linux下 一個 介紹 inux 版權聲明更新:2017-05-15博主:LuckyAlan聯系:[email protected]/* */聲明:吃水不忘挖井人,轉載請註明出處! 1 文章介紹 本文介紹了Linux下面
每天一個linux命令(9):cp命令
系統 lin 維基 介紹 參考 vip 聲明 com 開發平臺 版權聲明更新:2017-05-13博主:LuckyAlan聯系:[email protected]/* */聲明:吃水不忘挖井人,轉載請註明出處! 1 文章介紹 本文介紹了Linux下面的cp命令。
springmvc學習筆記(一) -- 從零搭建,基礎入門
out hand char webapp core localhost list ges del 1、新建maven項目 參考mybatis學習筆記(五) -- maven+spring+mybatis從零開始搭建整合詳細過程(上)第一部分,修改配置 2、修