The front-end framework of this article uses RAX, and the entire code is not open source (to be opened)
preface
How do you design a React /vue component? This article is not a theoretical film, more is my step by step thinking and practice. This article will have a lot of the author’s thinking process, welcome to the comments section more exchanges and discussion.
From requirements discussion, technical solution discussion to coding, to the final test, I experienced many brain bursts and encountered many pits, some of which may be related to the business, some may be related to the framework. Based on these pits, I discussed many solutions and very hack countermeasures. However, as time goes by, looking back at hack code at that time, many people don’t quite remember why they wrote this, so here is a brief record of the development process of Filter component. In order to later query, more hope that we can discuss together, in order to obtain better code architecture and implementation ideas.
Since the code is written using the raX framework based on the underlying WEEX, there are some pitfalls that you may not encounter if you are using React or Vue and can simply ignore
Tell me about the business
A Filter, the most common component, is, as the name suggests, a Filter. Let’s start by looking at some filter representations on existing apps. To make a component, we need it to be universal enough and easy enough to extend.
- Ali auction Filter
- Flying pigs in the Filter
Before we talk about the business characteristics of Filter, let’s constrain the naming of each section to make it easier for you to read:
Above are the filter pages of auction and flying pig respectively. From these two pages, we can probably summarize the following business portraits about Filter:
- As the page scrolls, the Filter may be adsorbed, but it may be at some distance from the top
- Panel Diversity (Click navItem to expand the Panel)
- Panel and navItem can also be animated
- NavBar content is mutable
- The display form of panel varies
- Panel Panel content can be very complex and needs to be considered for performance optimization
- There may be non-filter content on navBar (focus on buttons)
- Some navBar navItems have no corresponding Panel
- There is a “fast-sort” button on Filter that affects search results but does not
- The filter configuration parameter can be specified
- The panel selection can be initialized by passing the relevant filter ID through the URL
- .
Final component output
Because the raX 1.0TS +hooks open source version is still under development, the repository link will not be put for now
- Rax-pui-filter-utils: Internal tool library of filter, provided only by filter developers
- Rax-pui-filter-tools: Use some tool sets of filter, such as HOC component and placeholder component to improve performance (available or not, according to their own business needs), thinking reasons: Not every Filter user needs these features to be pluggable in order to reduce unnecessary bundle size
- Pui-filter: filter core function development library
Effect:
The query parameters thrown are visible on the console
Design and Thinking
Front-end Component Architecture Diagram (first version)
Component Architecture Diagram (endboard)
├─ navBar ├─ navBase.js // SRC ├─ file.js //Filter Out the parent container Constant.js // item code constants Definition ├─ index.js // import file ├─ navBar // Navbar folder │ ├─ navBase.js //navBar base class NavQuickSearch and NavRelatePanel parent class │ ├─ NavQuickSearch.js //navBar base class │ ├─ NavRelatepanel.js // ├─ ├─ ├─ style.js // ├─ ├─ style.js // navBar │ ├─ ├─ style.js // navBar │ ├─ ├─ style.js // navBar │ ├─ ├─ style.js // navBar │ ├─ ├─ style.jsCopy the code
Component features
- The Filter header UI is dynamically configurable and extensible, supports click animation, and provides three filter item types
RelatePanel
:Filter items associated with Panel type, that is, there is a one-to-one relationship between the filter head and the Panel. Click the filter head to display the PanelQuickSearch
:Filter items quickly search sorted type, that is, the filter header does not correspond to the Panel, click the filter header to trigger the search directlyPureUI
:Pure UI placeholder type, pure UI placement that does not involve searching, such as the subscribe button scenario
- Screen panel display and hide unified management, support drop down and left slide to show hidden animation, unified search callback function
- The Filter component is isolated from the service panel and supports the access of any component
onChange(params)
The callback function to trigger - Three business-common panel components are provided
rax-pui-list-select
List to select the business panelrax-pui-location-select
, province and region cascade select business panelrax-pui-multi-selection-panel
, multi-select business panel,View the component usage documentation
This refers to the functional features of Filter. The functions of Filter components mentioned above may not be completely covered, but we provide solutions. The design of components always adheres to the principle of non-intrusion into business, and all configuration entries related to business are provided.
Expect components to use form
import Filter from 'rax-pui-filter'; Render (<Filter navConfig={[]} onChange={()=>{}}> < filter. Panel> < business component 1 /> </ filter. Panel> < filter. Panel> < business component 2 /> </Filter.Panel> </Filter> );Copy the code
Boundaries between component functionality and business requirements
What is a business function and what is a component function, this needs to be discussed in detail, in fact, there is no strict distinction. Basically, he gives you a charger when you buy a phone. But… Why do many cell phones come with cases (Xiaomi, Huawei, Honor) but not iPhone? So is it standard?
For our component, in a nutshell: what we can do, we do! But some of the features we tease out are data business features:
- What copywriting and styles are displayed by each navItem on navBar and belong to business functions
- The data processing of the whole Filter, including the query parameters on the URL, is also a business function that needs to be thrown to the copy displayed by the corresponding navItem
- Filter whether to click scroll to the top is also a business function, after all, many search page Filter itself at the top. Also, for RAX, different containers scroll differently (but we provide this method for you to call)
- Panel Data requests, logic processing are your own business logic. The Filter provides only basic container capabilities and interfaces
In other words, any function in a Filter can be said to be a business function. But we need to provide a Future that encapsulates functionality that 80% of the business needs as a Filter. That’s what we’re trying to do.
Based on the above distinction between business and component functions, we know what configuration and method you should pass to me when using Filter.
Filter API
parameter | instructions | type | Default value (mandatory) |
---|---|---|---|
navConfig | Filter header configuration,Click to view detailed configuration items rendering |
Array<Object> | – (Required) |
offsetTop | Filter height from the top of the page when the Filter component is expanded. There are two states:A fixed positionandFollow the page scroll adsorption top A fixed positionHeight from the top of the page in state Follow page scroll adsorption top:Height from the top of the page in state rendering |
Number | 0 |
styles | Configure styles. All styles in Filter are availablestyles Collection object to configure overridesStyles format |
Object | {} |
getStickyRef | Obtain the REF instance of the Sticky node for rolling adsorption scenarios and internal coordinationpm-app-plus Container components automatically attach to the top when clicking Filterfigure |
Function | |
keepHighlight | Whether the filter header needs to remain highlighted after the filter criteria change rendering |
Boolean | false |
clickMaskClosable | Turn on mask background click Hide | Boolean | true |
onChange | Filter searches the change callback function Signature: Function(params:Object,index:Number, urlQuery: Object) => void Parameters: params: Object The search parametersindex:Number Trigger the search Panel searchurlQuery:Object URL query object |
Function | |
onPanelVisibleChange | Panel shows the hidden callback function Signature: Function({ visible:Boolean, triggerIndex:Number, triggerType:String }) => void Parameters: visible:Boolean Display hidden flagstriggerIndex:Number Index value of the triggered filtertriggerType:String Trigger typeTriggerType,There are three trigger types Navbar : Click trigger from filter headerMask : Click trigger from the background layerPanel : The onChange callback from the Panel is triggered |
Function |
Filter Prop navConfig Array configuration details
navConfig
Filter item type type
RelatePanel
:Filter items associated with Panel type, that is, there is a one-to-one relationship between the filter head and the Panel. Click the filter head to display the PanelQuickSearch
:Filter items quickly search sorted type, that is, the filter header does not correspond to the Panel, click the filter header to trigger the search directlyPureUI
:Pure UI placeholder type, pure UI placement that does not involve searching, such as the subscribe button scenario
Note that if navConfig’s built-in UI parameters do not meet your requirements, use the renderItem custom rendering function to control the filter header UI
parameter | instructions | type | Default value (mandatory) |
---|---|---|---|
type | Filter item type Three types of RelatePanel : Filter item associated data panel typeQuickSearch : Filter quick search sort typePureUI : Pure UI placeholder type |
String | ‘RelatePanel’ |
text Pay attention to RelatePanel Type to take effect |
The filter header displays the copy Text overflow . show |
String | – (Required) |
icons Pay attention to RelatePanel Type to take effect |
Filter header icon: Normal and active ICONS The data format Object Type:
|
Object or String | – |
options Pay attention to QuickSearch Type to take effect |
Quickly search sorted data sources The data format |
Array | (required) |
optionsIndex Pay attention to QuickSearch Type to take effect |
Quick search sort type default selected index | String | 0 |
optionsKey Pay attention to QuickSearch Type to take effect |
Specifies the search key for quick search sort, used in the onChange callback | String | Do not provide an index that defaults to the current filter |
formatText | Text formatting function Signature: Function(text:String) => text Parameters: text: String Filter head copywriting |
Function | (text)=>text |
disabled | Disables filter header clicking | Boolean | true |
hasSeperator | Whether to display the right delimiter rendering |
Boolean | false |
hasPanel | Whether the current filter header has a corresponding panel | Boolean | true |
renderItem | Custom rendering Pay attention to Use when the configuration items provided do not meet your UI requirements Signature: Function(isActive:Boolean, this:Element) => Element Parameters: isActive:Boolean Filter whether the header is activethis:Element Filter header this instance |
Function | – |
animation | Animation configuration, using built-in animation Parameters that Pay attention toOnly one is currently built in |
Object | |
animationHook | User – defined animation hook function, used when the built-in animation does not meet the requirements Signature: Function(refImg:Element, isActive:Boolean) => text Parameters: refImg:Element Filter the REF instance of the header iconisActive:Boolean Filter whether the header is active |
Function | – |
Filter.Panel API
parameter | instructions | type | Default value (mandatory) |
---|---|---|---|
styles | Configuration style All styles in Filter are available styles Collection object to configure overrides |
Object | {} |
displayMode | Panel display format: full-screen, drop-down Parameters that Full screen: Fullscreen Drop down: Dropdown |
String | ‘Dropdown’ |
noAnimation | Ban on animation | Boolean | true |
highPerformance | Internal control Panel through the display hide Panel render times, to avoid unnecessary render, high performance mode, only when the Panel display or display hidden state changes will be rerender | Boolean | true |
animation | Panel displays animation configuration, built-in up, down, left and right animation Parameters that |
Object |
Filter code use
- Filter parameter configuration
navConfig: [
{
type: 'RelatePanel', / /typeThis parameter is optional. The default value is'RelatePanel'
text: 'down', // Configure the filter header copy ICONS: {// Configure ICONS, divided into the normal form and click the selected form normal:'//gw.alicdn.com/tfs/TB1a7BSeY9YBuNjy0FgXXcxcXXa-27-30.png',
active: '//gw.alicdn.com/tfs/TB1NDpme9CWBuNjy0FhXXb6EVXa-27-30.png',
},
hasSeperator: true// Display the vertical separator formatText: text => text +'left', // Filter text formatting function}, {type: 'QuickSearch',
optionsIndex: 0,
optionsKey: 'price', options: [// quicksort list {text:'price',
icon: ' ',
value: '0',
},
{
text: 'ascending',
icon: '//gw.alicdn.com/tfs/TB1PuVHXeL2gK0jSZFmXXc7iXXa-20-20.png',
value: '1',
},
{
text: 'descending',
icon: '//gw.alicdn.com/tfs/TB1a7BSeY9YBuNjy0FgXXcxcXXa-27-30.png',
value: '2',},],}, {type: 'RelatePanel', / /typeThis parameter is optional. The default value is'RelatePanel'
text: 'rotation'// ICONS: {// set ICONS into the normal form and click on the selected form normal:'//gw.alicdn.com/tfs/TB1PuVHXeL2gK0jSZFmXXc7iXXa-20-20.png',
active: '//gw.alicdn.com/tfs/TB1l4lIXhv1gK0jSZFFXXb0sXXa-20-20.png',
},
animation: { type: 'rotate'}, // Configure animation after click rotate image, default no animation}, {type: 'RelatePanel', / /typeThis parameter is optional. The default value is'RelatePanel'
text: 'the left'}, {type: 'PureUI',
text: 'subscribe'RenderItem: () => {// Render custom UIreturn (
<Image
style={{
width: 120,
height: 92,
}}
source={{ uri: 'https://gw.alicdn.com/tfs/TB1eubQakL0gK0jSZFAXXcA9pXa-60-45.png'}} / >); },},] // <Filter offsetTop={100} // offsetTop= RecycleView The current value is 100 navConfig={this.state.navConfig} // Filter Navbar configuration item keepHighlight={true} // Keep the changes highlighted styles={styles} // Config override the built-in styles, Big style object collection onChange = {this. HandleSearchChange} / / Panel Panel shows the change event onPanelVisibleChange = {this. HandlePanelVisibleChange} > <Panel highPerformance={true}> <ListSelect {... this.state.data1} /> </Panel> <Panel> <LocationSelect {... this.state.data2} /> </Panel> <Panel displayMode={'Fullscreen'} animation={{// Animation ={// timingFunction:'cubic - the bezier (0.22, 0.61, 0.36, 1)',
duration: 200,
direction: 'left'}}> <MultiSelect {... this.state.data3} /> </Panel> </Filter>Copy the code
Code operation effect diagram as shown in the screenshot above. Now, a brief description of the code implementation.
Core source code Display
The open source version (Ts+hooks+ Lerna) is not yet available, so the code is still being written in rax 0.x. This is only done where there are holes in the code processing explanation. Welcome to comment and leave your thoughts
Filter.js
Let’s start with the Render method
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>
);
}
Copy the code
Get some basic configuration, as well as the windowHeight (screen height) and activeIndex (which item is currently active (clicked)).
RenderPanels are written on NavBar because in WEEx zIndex is disabled. To render A above B, A must come after B. This is written for the drop-down drawing of the panel, which appears to come out from under the navBar.
The renderPanel method renders the corresponding panel
/** * Render 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;
});
};
Copy the code
To be precise, this is HOC, and we pass to the Panel the props that the agent, the translator, or the Filter needs to use. Such as onChange callbacks, or panel hiding callbacks, and which panel needs to be expanded.
Due to the Panel complexity we do not know. In order to avoid continuous expansion and collection of unnecessary render, we adopt transform to remove the panels that do not need to be displayed from the screen, and move the panels that need to be displayed to the inside of the screen. See the render return of Panel
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>
);
Copy the code
For example, we all know that render is the most wasteful of the page performance, and when the page is initialized, the name of the Panel is not displayed (now the Panel is outside the screen), so do we need to render the Panel? However, in the current way of writing, the life cycle of the Panel component will go all the way. However, if the Panel needs to request data, and the query parameter locationId=123 in the URL of the page, navItem needs to display the corresponding geographical location. If the Panel is not rendered, how to get the name of the Panel according to the ID and pass it to navItem to display? Yes, we can intercept the Render method of the Panel, let the Panel render null, and the rest of the life cycle will run as usual. However, if a user uses ref in render, it can cause bugs that are hard to troubleshoot.
So finally, in order to increase the interactivity rate of the page without affecting the page requirements, we provided an optional tool: Performance HOC. Note that this is optional.
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 />; }}}; }Copy the code
Tell me if it’s the first time to block Render by configuring the Panel’s shouldInitialRender attribute.
Of course, Panel also has many other pits, for example, now Panel in order to repeat render, remove Panel off the screen, then, animation from the top to set the initial animation flash screen how to deal with?
The code for Filter is to initialize, format, check and verify various parameters, and communicate between Panel and NavBar, such as format and handleNavbarPress
NavBar core code
NavBar architecture
The core code
Probably can be seen from the architecture diagram, in the NavBar through different configurations, showing different types, NavBarItem NavQuickSearch, NavRelatePanel
Here are some things to note: The NavBar data is passed in through Filter props. If the state is managed by the parent component of the Filter, ShouldComponentUpdate of the Panel layer is provided. Also for high cohesion and low coupling of the component design, we encapsulated the props passed in to the state of the NavBar. Manage your own state.
constructor(props) { super(props); const navConfig = formatNavConfig(props.navConfig); this.state = { navConfig, }; } // We provide the internal formatNavConfig method, which depends on the business requirements of different componentsCopy the code
Another thing to note in NavBar is the passive update: the text on the NavBar is updated when the Panel layer is clicked, because here we use the parent component to communicate between the Panel and NavBar
// filter.js calls NavBar methods /** * Updates NavBar text */ handleNavTextChange = (index, navText, isChange =true) => {// Navbar render is removed to internal processing, Can reduce a Filter. A Panel of the additional render this. AsyncTask (() = > {this. RefNavbar. UpdateOptions (index, navText isChange); }); }; // navbar.js provides updateOptions /** * to the filter. js call to update navConfig, and the Filter component calls * asynchronouslysetState circumvents RAX framework bugs: The user calls the this.props. OnChange callback in componentDidMount to re-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,
};
}
returnitem; })}); }, 0); };Copy the code
Finally, there are two kinds of NavBar items: quick search and NavBarItem with panel. However, for its common functions, such as rendering UI logic, the method adopted here is to extract NavBase components. Provides NavQuickSearch and NavRelatePanel calls:
- NavBase part code
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} />),
];
};
Copy the code
- 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> ); }}Copy the code
Panel core code
The core function of Panel is to add basic functions to user-defined Panel. Child, such as background mask and animation timing processing.
Use of Panel:
<Panel
displayMode={'Fullscreen'} animation={{// Animation ={// timingFunction:'cubic - the bezier (0.22, 0.61, 0.36, 1)',
duration: 200,
direction: 'left'}}> <MultiSelect {... this.state.data3} /> </Panel>Copy the code
We provide the basic animation configuration, but also the animation functionhooks, depending on when the animation is triggered
get animationConfig() {
const { animation } = this.props;
if(! animation || ! is('Object')(animation)) {
return PANEL_ANIMATION_CONFIG;
}
returnObject.assign({}, PANEL_ANIMATION_CONFIG, animation); } / /... / * * * * to perform 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)',}}); }); }}}Copy the code
Since animation execution takes time, we should lock NavBar in Filter during this period, and the concept of lock is also provided to users. After all, we will not invade the business logic, and NavBar should be locked when no result is returned in the last search. Disallow re-clicking (although the user can handle this in the onchange callback function, it should also be considered and provided as a component), as well as for animations, which should disallow re-clicking of NavBar while the animation is executing. The animation configuration above looks like this:
There is also the core processing in Panel, which is probably about the processing of animation timing. In front of the trigger animation, for example, we need to set up animation initial state, but if the following wording, there will be a Panel flashing phenomenon, after all, we through the back door of the second event training in rotation to perform initialization, so here, if the user configuration to start the animation, so we need in the outermost layers of the Panel to add a visible flag: Opacity is set to 0 by default. After setting opacity of the outer container to 1, the Panel still flashes, but you can’t see it.
// Set the initial animation stylesetTimeout(() => {
setNativeProps(node, { style: { transform: ! visible ?'translate(0, 0)': v, }, }); }, 0); // Perform the animationsetTimeout(() => {
transition(
node,
{
transform: visible ? 'translate(0, 0)' : v,
},
{
timingFunction: timingFunction,
duration: duration,
delay: 0,
},
cb,
);
}, 50);
Copy the code
Set the animation initialization style to add:
setNativeProps(findDOMNode(this.refPanelContainer), {
style: {
opacity: 1,
},
});
Copy the code
conclusion
The component of Filter seems simple, but if you want to write a more general and extensive Filter component in the market, not only the granularity, coupling degree and performance of the component need to be considered, but also there are too many business logic need to be considered. For the current initial version (which has not been modified into the official open source version), it has basically covered the current business scenarios we can think of, and relevant businesses have been implemented.
Of course, if it is used directly in business rather than as an open source component, we can lower the level of the Child under the Panel through renderPortal, and manage the data state through EventBus, Redux, Mobx, etc. That will make the whole code logic look a lot clearer. However, in order to reduce the bundle size, we tried to minimize the use of generic packages and dependencies on third-party plug-ins.
Any ideas that are not mentioned in this article or that you have a better way to deal with these Filter business requirements are welcome in the comments section
Technical communication
Welcome to wechat public number: full stack front selection, daily access to high-quality articles push. You can also add my personal wechat communication ~