数据服务于组件
数据服务于组件
创建人 | 创建时间 | 内容说明 | 更新时间 |
---|---|---|---|
adsionli | 2023-04-25 | 开发中的数据服务于组件的思考 | 2023-04-25 |
就如标题所说的,数据服务于组件,那么究竟是什么意思呢?我想大家一开始肯定会想不明白,这也是我在这段时间刚进入公司工作后,文哥帮review代码时经常会提到的一个问题。说实话,在过往的开发中,我也写过很多的组件,但是从来没有认真的思考过究竟什么是数据服务于组件这个东西是什么,但是在最近文哥review时的引导下,好像开始慢慢明白了就究竟是什么意思了。
下面就从我实际工作开发时,遇到的例子进行展开说说。
内容开发

图1:物流信息展示
上面这张图就是要完成的一个功能,看起来不难实现吧,只有三个部分:头部检索、主体物流信息列表、尾部分页设置。
对于主体部分的物流信息列表,还包括了两个小功能,一个是hover图片的时候出现气泡框,还有一个功能就是点击查看物流时,弹出对应的dialog框,显示相关物流信息。具体如下两张图:

图2:气泡框功能

图3:dialog显示物流详情
我相信,大家一定都可以很快速的实现这些功能,如果实在不考虑相关数据设计以及性能优化的话。
下面就是我在几个小时内撸出来的代码,大家可以看看里面存在什么问题。
import './logisticsInfo.css';
import Card from './cardGroup/card';
import { titleList, infoList } from './data/test';
import React from 'react';
import Dialog from './dialog';
import Popover from './popover';
import DialogInfo from './infoShow/dialogInfo';
class LogisticsInfoList extends React.Component {
state: {
showDialog: boolean;
showPopover: boolean;
popoverPos: {left: number, top: number};
showImage: {url: string, context: string};
dialogTitle: string;
}
cardContent: any;
constructor (props: any) {
super(props);
this.state = {
showDialog: false,
showPopover: false,
popoverPos: { left: 0, top: 0 },
showImage: { url: '', context: '' },
dialogTitle: '物流信息',
};
this.cardContent = [
() => <div className='goods-image'>
<img onMouseEnter={(event) => {
this.handlePopoverShow(event, infoList[2].item_memo);
}} onMouseLeave={this.handlePopoverClose.bind(this)} src={infoList[0].pic_path} width="60px" height="60px" />
</div>,
() => {
const { tid, pay_time, consign_time } = { ...infoList[1] };
return <div className='orderInfo'>
<p className='orderInfoLabel'>订单编号:{tid}</p>
<p className='orderInfoLabel'>成交时间:{pay_time}</p>
<p className='orderInfoLabel'>发货时间:{consign_time}</p>
</div>;
},
() => <p className="goodsInfoLabel">{infoList[2].item_memo}</p>,
() => {
const { accountLink, receiver_name, receiver_mobile, receiver_address } = { ...infoList[3] };
return <div className='cosumerInfo'>
<a className='cosumerInfoFont cosumerInfoLink' href="">{accountLink}</a>
<p className='cosumerInfoParagraph cosumerInfoFont'>{receiver_name}</p>
<p className='cosumerInfoParagraph cosumerInfoFont'>{receiver_mobile}</p>
<p className='cosumerInfoParagraph cosumerInfoFont'>{receiver_address}</p>
</div>;
},
() => {
const { sid, logistics_company, logistics_info } = { ...infoList[4] };
return <div className='logisticsInfo'>
<div className='logisticsInfoHeader'><div>{logistics_company}</div> <div>运单号:{sid}</div></div>
<p className='logisticsInfoContext'>{logistics_info}</p>
<div className='logisticsInfoButtonGroup'>
<button onClick={this.handleDialog.bind(this)}>查看物流</button>
<button>发货短信提醒</button>
<button>我要群发</button>
</div>
</div>;
},
];
}
/** 处理popover气泡框显示数据 */
handlePopoverShow (event: any, context: string) {
const { width, left, top } = event.target.getBoundingClientRect();
const showInfo = {
left: left + width,
top,
};
this.setState({ showPopover: true });
this.setState({
showImage: {
url: event.target.src,
context,
},
});
this.setState({ popoverPos: showInfo });
}
/** 关闭气泡框 */
handlePopoverClose () {
this.setState({ showPopover: false });
}
/** 处理弹出dialog */
handleDialog () {
this.setState({ showDialog: !this.state.showDialog });
}
/** 复制图片 */
async copyImage () {
const { clipboard } = navigator;
const imageData = await fetch(this.state.showImage.url);
const blob = await imageData.blob();
const clipboardItem = new ClipboardItem({ [blob.type]: blob });
clipboard.write([clipboardItem]);
}
render () {
const popoverChildren = (<div className='popover-context'>
<div className='popover-image'>
<img src={this.state.showImage.url} width="260" height="260" />
</div>
<div className='popover-copy-button'>
<button className='copy' onClick={this.copyImage.bind(this)}>复制图片</button>
</div>
<div className='context-label'>{this.state.showImage.context}</div>
</div>);
return <section className="infoContiner">
{Array(10).fill('dddd')
.map((val: string, index: number) => <section className="infoItem" key={index}>
<main className='cradGroup'>
{
titleList.map((val: {name: string, width: string | number}, idx: number) => {
return <Card title={val.name} width={val.width} key={`${index}-${idx}`}>
{{ render: this.cardContent[idx] }}
</Card>;
})
}
</main>
<footer>卖家备注:<span>{val}</span></footer>
</section>)}
<Dialog showClose={true} show={this.state.showDialog} onCancle={this.handleDialog.bind(this)} title={this.state.dialogTitle} >
{{ body: <DialogInfo logisticsInfo={infoList[4]} /> }}
</Dialog>
<Popover show={this.state.showPopover} position={this.state.popoverPos} width={300}>
{popoverChildren}
</Popover>
</section>;
}
}
其中
Card
,Dialog
,DialogInfo
,Popover
均是我自己封装的组件。
臃肿的一大坨代码,完全没有做到代码的拆分,将很多JSX.Element
代码放在了一个context
数组中,完全没有进行解耦,导致数据完全与展示标签混在一起。了。然后还肆意的将Popover
组件和Dialog
组件全部放在了这个主文件中。混淆了这两个外部组件的作用边界,完全没法很好的对这些内容进行整合。
那么现在我们需要怎么去修改代码吗?在文哥帮助我review后,有了一些思路,然后我画了一张图,来协助我来对代码重构(简单的图,毕竟不是做Ui的,哈哈哈,大家看得懂就行)
图4:具体功能分析及数据流向分析
上图应该还是很清晰的表达出了整个数据流向以及每个组件需要完成的功能,这个时候我们可以发现,对于popover
以及dialog
的使用,我们直接将其放在了只有他们需要被使用的组件下,而不是直接放在list
这个主体列表下了,这样popover
与dialog
的组件功能的职责就很清晰,他们只会分别为goodsImage
与logisticsInfo
进行服务了,而不会因为list
这个主体列表影响。
其中goodsImage,ordersInfo,goodsInfo,buyerInfo,logisticsInfo
这分别传递给五个组件内容都是主体容器传递来的dataSource
数据拆分而来的。这样子我们就可以让每个组件各司其职,对于OrderInfoCard(对应orderInfo),GoodsInfoCard(对应goodsInfo),BuyerInfoCard(对应buyerInfo)
来说他们实际只会受到list
的props
过来的数据控制,他们三个本身是一个无状态组件,同理我们还可以知道list
这个组件也是一个无状态组件了,它只会因为主体容器组件传递来的dataSource
数据的改变而改变。
header
检索功能这个组件比较特殊,它其实不需要接收主题容器传递来的props
数据,它可以自己维护这些数据,然后再这些数据发生改变的时候,通过props
来的dispatch函数,直接发给主体容器,主体容器接收到之后,直接发起请求就可以了,所以在header
组件中,我们可以自己去维护相关检索条件数据,减轻主体容器负担。
footer
分页功能为什么和header
中自己管理数据不同呢,这是因为分页功能中的pageNo,pageSize
同时也会影响检索时候的数据返回。如果说,我们让footer
拥有自己的状态,也就是自己管理pageNo,pageSize
,那么当我进行检索操作的时候,我们还需要再去获取footer
这个子组件下的pageNo,pageSize
,这相当于主体容器当了一次转发器,那么这样的话,不如把状态保存在主体容器组件中。然后每一次footer
进行了操作之后,都会dispatch
到主体容器进行修改状态数据就可以了,这样footer
最终也就是一个无状态容器啦。
最终的代码实现,就放出一部分的内容,其余的可以去仓库里面进行查看
src目录下的
components,logisticsInfo,service
主体容器
import React from 'react';
import './index.scss';
import { isEmpty } from 'utils/index';
import { TRADE_SORT_BY } from 'tradePublic/consts';
import { getTradeSearch } from 'tradePublic/tradeDataCenter/api/searchListGet';
import { getTradeList } from 'tradePublic/tradeDataCenter/api/baseListGet';
import { fillFullinfo, getLogisticsInfo } from 'pages/tradeManagement/tradeList/service';
import LogisticsInfoList from './list';
import { handleLogisticsData } from '../../service/logisticsServices/handleLogisticsData';
import FooterPageChange from './retrieval/footer';
import HeaderTimePicker from './retrieval/header';
const InitPageInfo = {
INIT_PAGE_NO: 1,
INIT_PAGE_SIZE: 20,
};
/**
* @Description: 物流管理页面
* @author DCD
* @date 2021/3/26
*/
class LogisticsManagement extends React.Component {
constructor (props) {
super(props);
this.state = {
dataSource: [],
total: 0,
pageNo: InitPageInfo.INIT_PAGE_NO,
pageSize: InitPageInfo.INIT_PAGE_SIZE,
};
this.searchArgs = {};
}
/**
* @Description: 加载订单
* @author DCD
* @date 2021/3/25
*/
loadListData = async ({ pageNo = InitPageInfo.INIT_PAGE_NO, pageSize = InitPageInfo.INIT_PAGE_SIZE }) => {
let res;
// SELLER_CONSIGNED_PART
if (!isEmpty(this.searchArgs)) {
this.searchArgs.status = 'WAIT_BUYER_CONFIRM_GOODS';
this.searchArgs.timeFilterBy = 'create_time';
// 如果有搜索参数
res = await getTradeSearch({
pageSize,
pageNo,
searchArgs: this.searchArgs,
});
} else {
res = await getTradeList({
status: 'WAIT_BUYER_CONFIRM_GOODS',
pageSize,
pageNo,
sortBy: TRADE_SORT_BY.create_time_desc.key,
});
}
if (res.source == 'top') {
await fillFullinfo(res.trades);
}
// 加载物流信息
await getLogisticsInfo(res.trades);
const handleData = handleLogisticsData(res.trades);
this.setState({
dataSource: handleData,
total: res.totalResults,
});
}
componentDidMount () {
const { pageNo, pageSize } = this.state;
this.loadListData({ pageNo, pageSize });
}
/**
* @function handleRetrievalData 处理检索数据
* @param {SearchCondition} value 检索数据
*/
handleRetrievalData (value) {
this.searchArgs = { ...value };
const { pageNo, pageSize } = this.state;
this.loadListData({ pageNo, pageSize });
}
/**
* @function handleChangePage 处理分页数据变化
* @param {{pageNo: number, pageSize: number}} value
*/
handleChangePage (value) {
this.setState({
pageNo: value.pageNo,
pageSize: value.pageSize,
});
this.loadListData({ pageNo: value.pageNo, pageSize: value.pageSize });
}
render () {
const { dataSource, total, pageSize, pageNo } = this.state;
return (
<div className='logistics-management' >
<HeaderTimePicker retrievalData={this.handleRetrievalData.bind(this)} />
<LogisticsInfoList listData={dataSource} />
<FooterPageChange pageNo={pageNo} pageSize={pageSize} changePage={this.handleChangePage.bind(this)} total={total} />
</div>
);
}
}
export default LogisticsManagement;
list组件
import React from 'react';
import './list.scss';
import GoodsImageCard from '../card/goodsImage';
import TradeInfoCard from '../card/tradeInfo';
import GoodsTitleCard from '../card/goodsTitle';
import BuyerInfoCard from '../card/buyerInfo';
import LogisticsInfoCard from '../card/logisticsInfo';
interface LogisticsListProps {
// eslint-disable-next-line no-undef
listData: LogisticsInfoListWithMemo[];
}
/** 物流列表 */
class LogisticsInfoList extends React.Component<LogisticsListProps> {
constructor (props: any) {
super(props);
}
/**
* @function isEqualList 判断列表是否发生更新
* @param {} newData 更新后数据
* @param {} oldData 更新前数据
* @returns {boolean}
*/
isEqualList (newData, oldData) {
// NOTE: 处理基础类型的值或是相同引用的值
if (newData === oldData) {
return true;
}
// NOTE: 如果是引用对象,提取key
const keys1 = Object.keys(newData);
const keys2 = Object.keys(oldData);
// NOTE: 如果属性名数量不相等,则两个对象不相等
if (keys1.length !== keys2.length) {
return false;
}
for (const key of keys1) {
// 递归调用 isEqual 进行深比较
if (!this.isEqualList(newData[key], oldData[key])) {
return false;
}
}
return true;
}
/**
* @function shouldComponentUpdate 判断是否需要更新list列表
* @param {Readonly<LogisticsListProps>} nextProps 更新后的Props数据
*/
shouldComponentUpdate (nextProps: Readonly<LogisticsListProps>): boolean {
const { listData: newListData } = nextProps;
const { listData } = this.props;
const newLength = newListData.length;
const listLength = listData.length;
if (newLength !== listLength) {
return true;
}
if (nextProps.listData.length === 0) return false;
const isEqual = this.isEqualList(newListData, listData);
return !isEqual;
}
/**
* @function getLogisticsCardList 获取需要渲染的卡片列表
*/
getLogisticsCardList () {
const { listData } = this.props;
return listData.map((val: LogisticsInfoListWithMemo, index: number) => {
const { data, memo } = val;
const { goodsImage, goodsInfo, buyerInfo, logisticsInfo, orderInfo } = data;
return (<section className="info-item" key={index}>
<main className='info-item-card-group'>
<GoodsImageCard url={goodsImage.picPath} context={goodsInfo.title} />
<TradeInfoCard {...orderInfo} />
<GoodsTitleCard {...goodsInfo} />
<BuyerInfoCard {...buyerInfo} />
<LogisticsInfoCard {...logisticsInfo}/>
</main>
{
memo.length < 2 ? (<footer>卖家备注:<span>{memo[0]}</span></footer>)
: (
<footer>
{memo.map((str: string, index: number) => {
const tid = orderInfo.tId.split(',')[index];
return (<p key={`${tid}`}>卖家备注 {index + 1}: <span>{str}</span></p>);
})}
</footer>
)
}
</section>);
});
}
render () {
const { listData } = this.props;
const listLength = listData.length;
return (<section className="info-continer">
{
listLength ? this.getLogisticsCardList() : <div className='info-nothing'>暂无数据</div>
}
</section>);
}
}
export default LogisticsInfoList;
header组件
/* eslint-disable no-undef */
import moment from 'moment';
import { Button, DatePicker, Input, Select } from 'qnui';
import React from 'react';
import './header.scss';
interface HeaderTimePickerProps {
// eslint-disable-next-line no-unused-vars
retrievalData: (value: SearchLogisticsFilterData) => void;
}
class HeaderTimePicker extends React.Component<HeaderTimePickerProps> {
selectOption: {value: string, label: string}[]
state: SearchLogisticsData
constructor (props : HeaderTimePickerProps) {
super(props);
this.selectOption = [
{ value: 'buyerNick', label: '订单号或用户昵称' },
{ value: 'receiverName', label: '收件人姓名' },
{ value: 'receiverMobile', label: '收件人手机或电话' },
{ value: 'waybillNum', label: '运单号' },
{ value: 'logisticsCompany', label: '物流公司' },
];
this.state = {
searchCondition: 'buyerNick',
inputPlaceholder: '订单号或用户昵称',
startTime: '',
endTime: '',
inputValue: '',
};
}
/** 过滤检索数据 */
filterData () {
const searchData = {};
const reg = new RegExp('^[0-9]{19}$');
for (const key of Reflect.ownKeys(this.state)) {
if (['searchCondition', 'inputPlaceholder'].includes(String(key)) || (this.state[key] as String).trim() === '') {
continue;
}
if (key === 'inputValue') {
const conditionKey: string = reg.test((this.state[key] as String).trim()) ? 'tid' : this.state.searchCondition;
searchData[conditionKey] = this.state[key];
} else {
searchData[key] = this.state[key];
}
}
return searchData;
}
/** 禁止选择的时间 */
disabledDate (calendarDate) {
const { year, month, date } = calendarDate;
const theDate = moment(`${year}-${month + 1}-${date}`, 'YYYY-M-D');
return Number(theDate) > new Date().getTime();
}
/** 处理开始时间选择器选择事件 */
handleStartTime (val: Date, str: string) {
this.setState({ startTime: str });
}
/** 处理结束时间选择器选择事件 */
handleEndTime (val: Date, str: string) {
this.setState({ endTime: str });
}
/** 处理选择框选择事件 */
handleSelectChange (value: string) {
const { label } = this.selectOption.find((val:{value: string, label: string}) => val.value === value);
this.setState({ searchCondition: value, inputPlaceholder: label });
}
/** 处理搜索框输入数据 */
handleChangeInputValue (value: string) {
this.setState({ inputValue: value });
}
/** 点击触发检索 */
handleClickSearch () {
const filterResult = this.filterData();
this.props.retrievalData(filterResult);
}
/** 重置检索数据 */
handleReset () {
this.setState({
searchCondition: 'buyerNick',
startTime: '',
endTime: '',
inputValue: '',
});
this.props.retrievalData({});
}
render () {
const { searchCondition, startTime, endTime, inputValue, inputPlaceholder } = this.state;
return (<div className='header-time-picker'>
<Select
style={{ width: '170px' }}
dataSource={this.selectOption}
value={searchCondition}
onChange={this.handleSelectChange.bind(this)} />
<Input
style={{ width: '240px' }}
placeholder={inputPlaceholder}
value={inputValue}
onChange={this.handleChangeInputValue.bind(this)} />
<div className='range-time-picker'>
<DatePicker
className="date-picker"
value={startTime}
showTime
onChange={this.handleStartTime.bind(this)}
disabledDate={this.disabledDate.bind(this)}
/>
<span>-</span>
<DatePicker
className="date-picker"
value={endTime}
showTime
onChange={this.handleEndTime.bind(this)}
disabledDate={this.disabledDate.bind(this)}
/>
</div>
<div className='button-group'>
<Button
type="primary"
style={{ margin: '0 5px' }}
onClick={this.handleClickSearch.bind(this)}>确定</Button>
<Button
type="secondary"
style={{ margin: '0 5px' }}
onClick={this.handleReset.bind(this)}>重置</Button>
</div>
</div>);
}
}
export default HeaderTimePicker;
footer组件
import TradePagination from 'components/tradePagination';
import React from 'react';
import './footer.scss';
import { Select } from 'qnui';
import { FOOT_PAGE_SIZE } from 'tradePublic/consts';
interface FooterPageChangeProps {
changePage: (value: PageInfo) => void;
total: number;
pageNo: number;
pageSize: number;
}
class FooterPageChange extends React.Component<FooterPageChangeProps> {
constructor (props: FooterPageChangeProps) {
super(props);
}
/** 处理页码改变 */
handleChangePageNo (value: number) {
const { pageNo, pageSize } = this.props;
if (pageNo === value) {
return;
}
this.props.changePage({ pageNo: value, pageSize });
}
/** 处理分页大小改变 */
handleChangePageSize (value: number) {
const { pageNo, pageSize } = this.props;
if (value === pageSize) return;
this.setState({ pageSize: value });
this.props.changePage({ pageNo, pageSize: value });
}
render () {
const { pageNo, pageSize, total } = this.props;
return <div className="footer-page-change">
<TradePagination
className="pageination-container"
current={pageNo}
pageSize={pageSize}
size="medium"
total={total}
onChange={this.handleChangePageNo.bind(this)}
/>
<Select style={{ margin: '0 10px' }} value={pageSize} onChange={this.handleChangePageSize.bind(this)}
dataSource={FOOT_PAGE_SIZE.batchPrint.map((page) => ({ value: page, label: `每页${page}单` }))}
/>
</div>;
}
}
export default FooterPageChange;
其中有些组件是公司项目组件库内容,所以大家应该看不到,只要明白意思就好啦,
有了上面的例子之后,我相信大家应该有些明白什么叫做数据服务于组件了吧,其实很好理解了,就是数据只需要绑定在需要使用的组件,这样无论在后期维护还是代码阅读,耦合性等都可以得到很好的提升,这也是我来到公司一周多两天做了两个小需求和文哥review code之后,学习到与在学校自己闭门造车开发时完全不同的东西。
特地整理成一篇文章,好让自己以后可以避免再犯组件边界模糊的问题以及数据乱使用的问题,也希望大家可以在这篇文章中收获到自己想要的内容。
虽然我们现在处于一个就业比较艰难的日子中,但是也希望大家能够继续提升自己的专业技能水平,当我们自身拥有过硬的技术的时候,不愁找不到工作呀,大家加油!!!冲冲冲!!!!
转载自:https://juejin.cn/post/7225898744448892986