故事背景
- 今天又写了一遍组件状态的持久化存储, 距离上一次写这个功能已经过去半年多了
- 为了方便此功能下次能够更快开发出来, 将此次组件代码抽象成模板记录下来
模板代码
// 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);
效果展示
亮点总结
1. 正确处理从localStorage中取数据的时候可能的报错
try ... catch ...
2. 保证历史数据中的内容不重复
在插入新的内容之前先对原来数组中的内容查找一遍,删除相同的元素。
3. antd中Tab组件的使用
根据持久化数据构建Tab的items中的子组件。
4. 组件内部高度封装,仅仅通过props传递的两个方法和外部通信
handleSelect和changeSearchResultVisible
5. 父组件通过一个字段控制此组件显隐
visible