React组件开发-京东618商品展示组件封装

711 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

1、前言

     正值京东618,不出意外地我也在京东买买买。在浏览商品的同时,我发现京东商品展示组件非常地美观,于是萌生了仿写这个组件的想法。

2、组件展示

先来看看这个商品展示组件长啥样吧


image.png
卡片的结构由轮播图、评价详情、标题、价格、底部操作栏构成,点击购物车或者立即购买展示商品配置组件
image.png
商品配置组件由商品信息,配置选项、数量以及一个确定按钮构成

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} />

Ⅱ、评价详情

评价详情主要是样式的展示,值得注意的是分割符以及多行文字溢出的处理。

  • 分隔符 image.png
    这里可以使用伪元素来做,使用伪元素的优点是可以很好控制分隔符的样式,如果使用border-right来做,它会占据整个盒子的高度。

image.png(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 必须结合的属性 ,设置或检索伸缩盒对象的子元素的排列方式 。

image.png

Ⅲ、价格(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)

我们先回顾一下商品配置组件,如下图:

image.png
主要是由商品信息、配置选项、数量选择以及确定按钮构成。

Ⅰ、商品信息

商品信息主要是商品图片和价格的展示,以及商品配置组件隐藏按钮。前文提到商品配置组件的展示与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/,欢迎大家浏览!