React组件中搜索数据持久化存储模板

194 阅读3分钟

故事背景

  • 今天又写了一遍组件状态的持久化存储, 距离上一次写这个功能已经过去半年多了
  • 为了方便此功能下次能够更快开发出来, 将此次组件代码抽象成模板记录下来

模板代码

// localStorage.setItem('source-search-history', JSON.stringify({"0":["11111","22222"],"1":["11111","22222"],"2":["11111","22222"],"3":["11111","22222"]}))  

import React from 'react';  
import { injectIntl } from "react-intl";  
import {  
  Tabs,  
} from "antd";  
  
import './SearchResult.less';  
 
const sourceTypes = {  
  "0": "tab0",  
  "1": "tab1",  
  "2": "tab2",  
  "3": "tab3",  
}  
  
const sourceNames = {  
  "tab0": 0,  
  "tab1": 1,  
  "tab2": 2,  
  "tab3": 3,  
}  
  
// tab页签中内容部分的标头,提供清除历史记录的功能  
const SearchTitle = (props) => {  
  // i表示tab页签的序列号  
  // handleClick表示点击清除按钮之后的回调  
  const { i, handleClick } = props;  
  return (  
    <div  
      style={{  
        display: 'flex',  
        width: '100%',  
        justifyContent: 'space-between',  
        marginBottom: 5,  
        cursor: 'default',  
      }}  
    >  
      <span style={{ color: 'rgba(0,0,0,0.45)' }}>历史记录</span>  
      {/* 清除历史记录的button */}  
      <span  
        onClick={  
          () => {  
            handleClick(i);  
          }  
        }  
      >  
        <button style={{width:'105%', height:'65%', backgroundColor: '#fff', lineHeight: '6px'}}>X</button>  
      </span>  
    </div>  
  )  
}  
  
interface IProps {  
  data: any;  
  rowHight: number;  
  intl: any;  
  searchResultShow: boolean;  
  resultScroll: any;  
  visible: boolean;  
  isInput: boolean;  
  handleSelect: any;  
  changeSearchResultVisible: any;  
}  
  
interface IStates {  
  searchHistory: Record<string, any>;  
}  
  
class SearchResult extends React.Component<IProps, IStates> {  
  state = {  
    visible: false,  
    // antd要求的item的格式  
    items: [  
      {  
        key: '1',  
        label: `tab1`,  
        children: [],  
      },  
      {  
        key: '2',  
        label: `tab2`,  
        children: [],  
      },  
      {  
        key: '3',  
        label: `tab3`,  
        children: [],  
      },  
      {  
        key: '4',  
        label: `tab4`,  
        children: [],  
      },  
    ],  
    // 只有组件销毁或者创建或者主动做持久化的时候才和localStorage交互  
    // 在组件正常显示的时候渲染信息从searchHistory变量中来,而不是localStorage中  
    searchHistory: {},  
  }  
  
  // this.props = props  
  constructor(props: IProps) {  
    super(props);  
  }  
  
  // 组件构建完成之后从localStorage的[source-search-history]字段中读出历史记录  
  // 从localStorage中拿值需要格外注意处理各种可能的报错  
  componentDidMount(): void {  
    // 持久化信息接受变量  
    let searchHistory = {  
      '0': [],  
      '1': [],  
      '2': [],  
      '3': [],  
    };  
  
    // 使用try catch处理反序列化  
    try {  
      const _a = localStorage.getItem('source-search-history') || '{}';  
      searchHistory = JSON.parse(_a);  
    } catch (e: unknown) { }  
  
    // 保证信息变量结构的正确性  
    if (!searchHistory[0]?.map) searchHistory[0] = [];  
    if (!searchHistory[1]?.map) searchHistory[1] = [];  
    if (!searchHistory[2]?.map) searchHistory[2] = [];  
    if (!searchHistory[3]?.map) searchHistory[3] = [];  
    // 将searchHistory存到state中去  
    this.setState({  
      searchHistory,  
    })  
  }  
  
  // 组件卸载之前将状态做持久化处理(信息存储在localStorage中)  
  componentWillUnmount(): void {  
    this.saveSearchHistory();  
  }  
  
  // 切换页签时候的回调  
  onTabChange = (key: string) => {  
    console.log(key);  
  };  
  
  // 更新历史数据值  
  // 先通过type找到需要更新哪个tab中的记录  
  // 然后查询这个tab对应的历史记录数组中是否已经有这个值  
  // 如果没有就放在数组头部,如果有先将原来的元素找出来删掉之后再放到数组头部  
  updateSearchHistory = (name: string, type: string) => {  
    const index = sourceNames[type];  
    const newSearchHistory = this.state.searchHistory;  
    const specificHistory = newSearchHistory[index];  
    const delIndex = specificHistory.findIndex(item => item === name);  
    if (delIndex !== -1) specificHistory.splice(delIndex, 1);  
    specificHistory.unshift(name);  
    // 更新state.searchHistory  
    this.setState({  
      searchHistory: newSearchHistory,  
    })  
    // state信息持久化  
    this.saveSearchHistory();  
  }  
  
  // 清空searchHistory中的某个数组  
  // 即将某个tabs中的历史清空  
  clearTypedSearchHistory = (index: number) => {  
    const newSearchHistory = this.state.searchHistory;  
    // 根据index找到需要清空的对象  
    const specificHistory = newSearchHistory[index];  
    // 注意这里千万不要用specificHistory=[]对specificHistory重新赋值  
    specificHistory.length = 0;  
    // 更新state.searchHistory  
    this.setState({  
      searchHistory: newSearchHistory,  
    })  
    // state信息持久化  
    this.saveSearchHistory();  
  }  
  
  // 将state信息持久化到storage中去  
  saveSearchHistory = () => {  
    localStorage.setItem('source-search-history', JSON.stringify(this.state.searchHistory));  
  }  
  
  // 渲染持久化数据  
  renderHistoryItems() {  
    const raw = { ...this.state.searchHistory }  
    // 深拷贝items模板  
    const items = JSON.parse(JSON.stringify(this.state.items));  
    // 遍历items构建用于在Tab组件上渲染的children组件  
    for (let i = 0; i < items.length; i++) {  
      if (raw[i]?.map) {  
        items[i].children = raw[i].map(  
          (item) => {  
            return <span  
              className='search-history'  
              onClick={(e) => {  
                const name = item;  
                const type = sourceTypes[i];  
                this.updateSearchHistory(name, type);  
                // 下面这两个方法都是从外部传递进来的,是可以定制化的内容,方便和外界交互  
                // 点击历史记录之后的回调  
                this.props.handleSelect(name, type);  
                // 点击历史记录之后Tab需要收起来  
                this.props.changeSearchResultVisible(false);  
              }}  
            >  
              {item}  
            </span>;  
          }  
        )  
        // 记得将标头元素添加到children数组的头部  
        items[i].children.unshift(  
          <SearchTitle i={i} handleClick={this.clearTypedSearchHistory} />  
        )  
      } else {  
        // 没有数据的时候只有标头  
        items[i].children.unshift(  
          <SearchTitle i={i} handleClick={this.clearTypedSearchHistory} />  
        )  
      };  
    }  
    return items;  
  };  
  
  render(): React.ReactNode {  
    // 父组件控制此组件显隐  
    const { resultScroll, visible } = this.props;  
    // 构建Tab需要的渲染用组件  
    const renderItems = this.renderHistoryItems();  
    return (  
      <div  
        style={{ display: `${visible ? 'block' : 'none'}` }}  
        className='searchResult'  
        onScroll={resultScroll}  
      >  
        <div  
          className='list-area'  
          style={{  
            position: 'relative',  
          }}  
        >  
          <Tabs  
            defaultActiveKey="1"  
            items={renderItems}  
            onChange={this.onTabChange}  
            size={"little"}  
          />  
        </div>  
      </div>  
    )  
  }  
}  
  
// 加入国际化  
export default injectIntl(SearchResult);

效果展示

history.gif

亮点总结

1. 正确处理从localStorage中取数据的时候可能的报错

try ... catch ...

2. 保证历史数据中的内容不重复

在插入新的内容之前先对原来数组中的内容查找一遍,删除相同的元素。

3. antd中Tab组件的使用

根据持久化数据构建Tab的items中的子组件。

4. 组件内部高度封装,仅仅通过props传递的两个方法和外部通信

handleSelectchangeSearchResultVisible

5. 父组件通过一个字段控制此组件显隐

visible