持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情
1、前言
正值京东618,不出意外地我也在京东买买买。在浏览商品的同时,我发现京东商品展示组件非常地美观,于是萌生了仿写这个组件的想法。
2、组件展示
先来看看这个商品展示组件长啥样吧
卡片的结构由轮播图、评价详情、标题、价格、底部操作栏构成,点击购物车或者立即购买展示商品配置组件
商品配置组件由商品信息,配置选项、数量以及一个确定按钮构成
3、封装步骤
看完了组件的展示,我们来动手封装一下这个组件。
3.1、项目创建
npm init @vitejs/app goodscard
初始化一个react项目脚手架,项目结构如下
goodscard
├── public/ # static files
│ └── index.html # html template
│
├── src/ # project root
│ ├── assets/ # css, icons, etc.
│ ├── components/
│ │ └── goods-card/
│ │ ├──buy-tab
│ │ ├──carousel
│ │ ├──price
│ │ ├──index.jsx
│ ├── App.jsx
│ ├── ...
│ ├── index.js
│
└── package.json
3.2、组件封装
首先是对goodscard组件的结构确定,由组件展示图片可以看到这个组件主要由轮播图、评价详情、标题、价格、底部操作栏构成
a、商品信息卡片组件(goods-card)
Ⅰ、轮播图(carousel)
轮播图这块可以使用Swiper、atnd-mobile或者其他UI组件库,我这边使用的是antd-mobile。这个轮播图需要完成在切换图片时显示图片对应的位置,例如一共5张图片,第一张图片展示1/5,第二张图片展示2/5。这个功能主要是使用antd-mobile中Swiper组件提供的onIndexChangeAPI,传入一个回调函数,在图片切换时调用,代码如下:
const Carousel = ({imgs}) => {
const [imgIndex, setImgIndex] = useState(1)
const items = imgs.map((item, index) => (
<Swiper.Item key={index}>
<img src={item} alt="" className='img-item' />
</Swiper.Item>
))
return (
<div className='imgWrapper'>
// 每切换一次图片,将imgIndex状态设为当前图片下标+1
<Swiper onIndexChange={index => setImgIndex(index+1)} indicator={() => null}>
{items}
</Swiper>
<span className='img-index'>{imgIndex} / {imgs.length}</span>
</div>
)
}
// 父组件的调用
<Carousel imgs={imgs} />
Ⅱ、评价详情
评价详情主要是样式的展示,值得注意的是分割符以及多行文字溢出的处理。
- 分隔符
这里可以使用伪元素来做,使用伪元素的优点是可以很好控制分隔符的样式,如果使用border-right来做,它会占据整个盒子的高度。
(border-right效果图)
- 多行文字溢出 一般单行溢出使用的是
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
要实现在指定行超出文字替换成省略号,使用如下代码:
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-clamp设置截断内容之前的最大行数,是一个 不规范的属性(unsupported WebKit property),它没有出现在 CSS 规范草案中。为了实现该效果,它需要组合其他外来WebKit属性:
display: -webkit-box; 必须结合的属性 ,将对象作为弹性伸缩盒子模型显示 。
-webkit-box-orient 必须结合的属性 ,设置或检索伸缩盒对象的子元素的排列方式 。
Ⅲ、价格(price)
价格这一块封装成一个子组件,因为在商品配置组件中仍会使用到,根据是否传入discount决定是否展示折扣价。实现代码如下:
const Price = ({price,discount}) => {
const priceArr = price.split('.')
return (
<div className="priceWrapper">
<span>¥</span>
// 整数部分
<span className="zhengshu">{priceArr[0]}</span>
// 小数部分
<span>.{priceArr[1]}</span>
{
// 根据是否传入discount属性来选择是否展示折扣价
discount &&
<span className="discount-price">
<span style={{ fontWeight: "normal" }}>到手价</span> ¥{discount}
</span>
}
</div>
);
};
Ⅳ、底部操作栏
底部操作栏分为左右两边区域,左边区域包含店铺入口按钮以及收藏按钮,右边区域包含加入购物车以及购买按钮。
- 收藏按钮
点击收藏样式切换,这个功能主要是利用classnames库来给DOM元素动态添加类名,当然使用style属性动态添加样式也是可以的。
使用
useState创建一个状态控制按钮样式,当状态值为true时,表示已收藏,则添加上collected类名,当值为false时,不添加额外的类。
import classNames from "classnames";
// 伪代码
const [hasCollect, setHasCollect] = useState(false);
<div className={classNames("collect tab-item",hasCollect && "collected")}>
<i className="iconfont icon-shoucang collect-font" />
<span className="item-font" onClick={() => setHasCollect(!hasCollect)}>
{hasCollect ? "已收藏" : "收藏"}
</span>
</div>
- 加入购物车以及购买按钮 这两个按钮都是用来控制商品配置组件的展示,组件的展示使用的是antd-mobile组件库的Popup组件,点击按钮,商品配置组件从底部弹出。做法与上面功能类似也是利用一个状态,当状态值为true时,展示组件,值为false时,隐藏组件。
const [showTab, setShowTab] = useState(false);
<Popup
visible={showTab}
onMaskClick={() => {setShowTab(false)}}
bodyStyle={{ height: "90vh" }}>
// 将商品配置组件的隐藏函数传递给该组件
{<BuyTab onClose={()=>setShowTab(false)} source={source} />}
</Popup>
b、商品配置组件(buy-tab)
我们先回顾一下商品配置组件,如下图:
主要是由商品信息、配置选项、数量选择以及确定按钮构成。
Ⅰ、商品信息
商品信息主要是商品图片和价格的展示,以及商品配置组件隐藏按钮。前文提到商品配置组件的展示与goodscard组件的showTab状态相关,所以隐藏功能的实现主要是父子组件之间的通信,子组件修改父组件的状态。可以利用父组件传递一个函数给子组件,这个函数用来修改状态,子组件再调用这个函数实现子组件修改父组件的状态。
// 回顾一下父组件对BuyTab调用并传入onClose和数据
<Popup
visible={showTab}
onMaskClick={() => {setShowTab(false)}}
bodyStyle={{ height: "90vh" }}>
{<BuyTab onClose={()=>setShowTab(false)} source={source} />}
</Popup>
// buy-tab组件中关闭按钮,绑定click事件,回调函数为传入的onClose函数
<i className='iconfont icon-guanbi font-close' onClick={onClose}></i>
⭐这里关闭按钮使用的是阿里的iconfont,感兴趣的同学可以点击学习
Ⅱ、配置选项(selection)
配置选项会被多次复用,所以这里把它封装成组件,便于复用。
组件分为标题以及选项,根据传入的数据对应展示,同时完成选项的单选功能。完成单选功能同样使用classnames库,创建一个selectedIndex状态,当点击一个选项时,将状态值设为当前子项的下标,并给子项添加一个三目运算符判断selectedIndex值是否为该子项的下标,若是,则添加selected类。
const Selection = ({data}) => {
const [selectedIndex, setSelectedIndex] = useState(-1)
return (
<div className='selectWrapper'>
<div className="title">
{data.title}
</div>
<div className="select-items">
{/* 每个服务的可选项 */}
{data.selection.map((item,index)=>(
<div
className={classNames("item",selectedIndex===index?'selected':'')}
key={index}
onClick={()=>setSelectedIndex(index)}>
{item}
</div>
))}
</div>
</div>
)
}
// buy-tab组件调用
info.map(item => (
<Select data={item} key={item.title}/>
))
总结
到这里就展示了一个简单的goods-card组件的封装,当然还可以继续完善这个组件,例如根据不同配置展示不同价格,但只要了解组件封装无外乎界面、样式、参数、插槽这四点,就能较好地实现功能。
这是我第一次写组件封装逻辑,可能还有很多不足,大家有不懂得都可以私聊我,另外这个组件的线上地址在https://pyf1999.github.io/,欢迎大家浏览!