UI组件封装是前端开发常用的代码复用手段,而「个人博客」与「仿手机QQ」是前端初学者常见的两个经典项目。本文内容来源于笔者对这两个经典项目在React框架下的个人实现,旨在展示笔者对React UI组件封装的理解,供读者参考。项目使用的基础UI组件库为Ant Desgin,封装的组件均为跨页面使用的组件。
一、「个人博客」项目
1.1 健壮图像 RobustImg
网页中引用的图像有时并不存在,此时<img>元素将显示为一个×。如果能用一张带有错误提示信息的图像替换它,就不会破坏页面布局,对用户也更为友好。
js
import { useState, useEffect } from 'react';
import DefaultImg from "../assets/error.png";
export default function RobustImg({src, style = {}}) {
const [imgSrc, setImgSrc] = useState(DefaultImg);
//尝试加载,成功则替换默认图片
useEffect(() => {
const image = new Image();
image.src = src;
image.onload = () => setImgSrc(src);
image.onerror = () => console.log(new Error('Could not load image at ' + src));
}, [src]);
return <img src={imgSrc} style={style} alt="" />
}
1.2 删除按钮 DelBtn
删除属于危险操作,与其他按钮相比,通常需要增加一步确认。为了减少重复代码,可将删除按钮单独封装,传入处理函数{onConfirm}作为props。
js
import { Button, Popconfirm } from "antd";
import { DeleteOutlined } from "@ant-design/icons";
const DelBtn = ({onConfirm}) => {
return (
<Popconfirm okText="确认" cancelText="取消"
title="确认删除?" onConfirm={onConfirm}
>
<Button type="primary" danger shape="circle" icon={<DeleteOutlined />} />
</Popconfirm>
)
}
export default DelBtn;
1.3 专栏下拉选择器 ColumnSelect
Select(下拉)选择器常用于可选项较多的场景,比如本项目中博客文章的专栏选择。如果某类数据的选择器在不同页面中多次出现,且选项动态生成,此时应将其封装为独立组件,并根据使用需求传入定制化参数作为props。
js
import { useState, useEffect } from "react";
import { Form, Select } from "antd";
import { http } from '../utils'; //http是axios.create返回的对象
const ColumnSelect = ({multiple = false, defaultValue = ''}) => {
const [columns, setColumns] = useState([]);
const [value, setValue] = useState(defaultValue);
useEffect(() => {
async function fetch() {
const res = await http.get('/columns');
setColumns(res.data);
}
fetch();
}, []);
return (
<Form.Item label="专栏" name="columns">
<Select
mode = { multiple ? 'multiple' : '' }
allowClear = { multiple }
value = {value} onChange = {v => setValue(v)}
defaultValue = {defaultValue} disabled = {defaultValue}
placeholder = "选择专栏"
options = {[
{ value: '', label: '不限' },
...columns.map(column => ({ value: column.name, label: column.name }))
]}
/>
</Form.Item>
);
}
export default ColumnSelect;
1.4 面包屑标题 BreadcrumbTitle
如果网站仅有一级导航,可以统一封装Breadcrumb面包屑组件,以{subtitle}作为外层组件的props。
js
import { Breadcrumb } from 'antd';
import { Link } from 'react-router-dom';
const BreadcrumbTitle = ({subtitle}) => {
return <Breadcrumb separator=">" items={[{ title: <Link to="/">首页</Link> }, { title: subtitle }]}/>
}
export default BreadcrumbTitle;
二、「仿手机QQ」项目
2.1 「更多」气泡弹出框 MorePopover
在移动APP右上角的区域,经常可以看见一个⊕按钮,点击它会弹出气泡式的菜单Popover.Menu。各个Popover入口的样式可以统一(比如使用MoreOutline),核心区别之处在于菜单的内容(actions)与点击处理函数(onAction)。actions与onAction紧密相关,可以定义在一个对象中,作为props传入自定义组件。
js
import { Popover } from 'antd-mobile';
import { MoreOutline } from 'antd-mobile-icons';
const MorePopover = ({moreActions}) => {
return (
<Popover.Menu
actions={moreActions.actions}
placement='bottom-start'
onAction={moreActions.onAction}
trigger='click'
>
<MoreOutline />
</Popover.Menu>
);
}
export default MorePopover;
2.2 消息时间 MsgTime
在手机QQ中,消息因发送时间不同显示为多种格式。简单起见,接下来只考虑两种情况:距当前时间不超过24小时的,时间格式为"HH:mm";否则时间格式为"MM/dd"。由于JS原生不支持日期格式字符串,时间数据的转换略显繁琐,因此应该自定义一个独立组件。
js
const MsgTime = ({timestamp, brief = false}) => {
if(!brief) return <span>{new Date(timestamp).toLocaleString()}</span>;
const msgTime = new Date(timestamp);
const timeDiff = new Date() - msgTime;
const hoursDiff = timeDiff / (1000 * 60 * 60);
return <div> { hoursDiff <= 24
? msgTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false })
: msgTime.toLocaleDateString([], { month: '2-digit', day: '2-digit' })
} </div>;
}
export default MsgTime;