写组件的能力是衡量前端工程师水平的重要指标,不管是基础组件还是业务组件。
笔者在空闲时间也喜欢写组件,为了帮助初学者上手写React组件,同时为了分享我在写组件中的经验和想法,决定开设一个系列,即:动手撸组件系列
,和大家分享一些公共组件和业务组件的实现方式和实现技巧。
作为这个系列的第一篇文章,分享下如何从零到一实现一个折叠面板(Collapse)组件
Collapse基础UI绘制
折叠面板作为一个基础组件,由两部分构成:第一部分是标题区域,第二部分是可折叠区域,点击标题区域可以折叠和展开内容区。为了组件的美观性可以在标题右侧添加一个箭头图标,在展开和折叠的时候使其旋转。
为了降低环境搭建成本,实践采用create-react-app
环境,创建create-react-app
开发环境异常简单,只需要在安装node
的系统中执行如下命令
npx create-react-app 项目名称
需要注意的是项目名必须为英文,create-react-app会自动为我们创建一个目录。
项目创建完成后,在src
目录下创建名为Collapse.jsx
的文件,输入如下代码:
(初学者可以选择复制)
import React, { useState } from "react";
import "./style.css";
const CollapsablePanel = () => {
const [isCollapsed, setIsCollapsed] = useState(true);
const togglePanel = () => {
setIsCollapsed((prevState) => !prevState);
};
return (
<div className="wrapper">
<div className="pannel" onClick={togglePanel}>
<div className="heading">
<span>Flower Collapse</span>
<svg width="20px"
height="25px" viewBox="0 0 1024 1024"
style={{ color: '#6495ed' }}><path d="M64 351c0-8 3-16 9-22.2 12.3-12.7 32.6-13.1
45.3-0.8l394.1 380.5L905.7 328c12.7-12.3 33-12 45.3 0.7s12 33-0.7 45.3L534.7
776c-12.4 12-32.1 12-44.5 0L73.8 374c-6.5-6.3-9.8-14.6-9.8-23z" p-id="1705">
</path></svg>
</div>
<div
className="content"
>
<div className="contentInner" >
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam
nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam
erat, sed diam voluptua.
</div>
</div>
</div>
</div>
);
};
export default CollapsablePanel;
接着创建名称为style.css
的样式文件
.wrapper {
display: flex;
justify-content: center;
width: 100%;
height: 100vh;
background-color: rgb(228, 239, 239);
padding-top: 40vh;
}
.pannel {
width: 400px;
text-align: left;
}
.heading {
background-color: #bfa;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
color: #000;
font-size: 20px;
line-height: 20px;
border: 1px solid rgb(212, 240, 205);
padding: 10px 20px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
}
.content {
font-size: 20px;
background: #fff;
border: 1px solid #fff;
border-top: none;
padding: 0 20px;
color: #000000;
overflow: hidden;
}
.contentInner {
padding: 20px 0;
}
创建完以上两个文件后,在index.js
中挂载创建好的Collapse组件:
(原有代码无关紧要,直接删除即可)
import React from 'react';
import ReactDOM from 'react-dom/client';
import Collapse from './components/Collapse';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Collapse />
);
创建完成后,使用yarn start
命令启动应用,就能看到绘制好的Collapse组件外观:
完成了Collapse基础UI绘制,回顾一下做了哪些操作:
首先定义了名为isCollapsed
的state,存储组件展开关闭状态,并声明了名为togglePanel
方法,在用户点击标题的时候调用此方法即可实现面板的展开关闭。
接着分别定义了样式名为pannel、heading、content的div容器及与其相关的子容器,并在style.css
中设置了容器的样式。组件中的ICON使用svg标签直接绘制,避免因引入svg包增大组件体积。
内容区展开动画
实现动画的方式有很多,可以使用css的transition属性实现,也可使用React生态种类繁多的动画库。在React生态中,有个非常流行的动画库叫react-spring
,不仅功能强大,而且支持hook方式调用,本文就用这个动画库来实现内容区域展开动画和按钮旋转动画。
安装react-spring
动画库
yarn add react-spring
安装完react-spring
动画库以后,就可以定义方法让spring帮我们生成动画样式了
const panelContentAnimatedStyle = useSpring({
height: isCollapsed ? 0 : 200,
});
接着把内容区域的标签名从<div>改为<animated.div>即可(和useSpring相同,animated也是react-spring具备的一个对象),并在标签中加上刚刚创建的panelContentAnimatedStyle
:
import React, { useState } from "react";
import { useSpring, animated } from "react-spring";
import "./style.css";
const CollapsablePanel = () => {
const [isCollapsed, setIsCollapsed] = useState(true);
const togglePanel = () => {
setIsCollapsed((prevState) => !prevState);
};
const panelContentAnimatedStyle = useSpring({
height: isCollapsed ? 0 : 180,
});
return (
<div className="wrapper">
<div className="pannel" onClick={togglePanel}>
<div className="heading">
<span>Flower Collapse</span>
<svg width="20px"
height="25px" viewBox="0 0 1024 1024"
style={{ color: '#6495ed' }}><path d="M64 351c0-8 3-16 9-22.2 12.3-12.7 32.6-13.1
45.3-0.8l394.1 380.5L905.7 328c12.7-12.3 33-12 45.3 0.7s12 33-0.7 45.3L534.7
776c-12.4 12-32.1 12-44.5 0L73.8 374c-6.5-6.3-9.8-14.6-9.8-23z" p-id="1705">
</path></svg>
</div>
<animated.div
style={panelContentAnimatedStyle}
className="content"
>
<div className="contentInner" >
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam
nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam
erat, sed diam voluptua.
</div>
</animated.div>
</div>
</div>
);
};
export default CollapsablePanel;
点击标题栏可以看到如下效果:
等等~~ 高度是固定的吗?
显然不是!用户在使用Collapse组件的时候,传递的内容不单是文字还有可能是图片或者是任意类型的ReactNode,所以在展开的时候是需要获取content对象的实际高度,获取DOM对象高度的操作有一个库可以帮助我们,react-use-measure,这个库不仅可以测量DOM对象的长度和宽度,还可以测量DOM对象距离浏览器上下左右的位置。react-use-measure
提供了名为useMeasure
的hook,使用方式如下:
const [ref, bounds] = useMeasure();
第一个参数是ref对象,将其绑定到需要测量的DOM对象的ref属性上即可,第二个bounds就是位置对象,包含上面提到的所有属性。
继续改造组件代码Collapse.jsx
:
import React, { useState } from "react";
import { useSpring, animated } from "react-spring";
import useMeasure from 'react-use-measure'
import "./style.css";
const CollapsablePanel = () => {
const [isCollapsed, setIsCollapsed] = useState(true);
const [ref, bounds] = useMeasure();
const togglePanel = () => {
setIsCollapsed((prevState) => !prevState);
};
const panelContentAnimatedStyle = useSpring({
height: isCollapsed ? 0 : bounds.height,
});
return (
<div className="wrapper">
<div className="pannel" onClick={togglePanel}>
<div className="heading">
<span>Flower Collapse</span>
<svg width="20px"
height="25px" viewBox="0 0 1024 1024"
style={{ color: '#6495ed' }}><path d="M64 351c0-8 3-16 9-22.2 12.3-12.7 32.6-13.1
45.3-0.8l394.1 380.5L905.7 328c12.7-12.3 33-12 45.3 0.7s12 33-0.7 45.3L534.7
776c-12.4 12-32.1 12-44.5 0L73.8 374c-6.5-6.3-9.8-14.6-9.8-23z" p-id="1705">
</path></svg>
</div>
<animated.div
style={panelContentAnimatedStyle}
className="content"
>
<div ref={ref} className="contentInner" >
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam
nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam
erat, sed diam voluptua.
Lorem ipsum, dolor sit amet consectetur adipisicing elit.
Quasi aperiam dignissimos eaque deserunt expedita sit
accusamus sunt laudantium repellendus nisi! Sit,
consequuntur. Tempora, officiis molestiae
fuga sit quae aliquid maxime.
</div>
</animated.div>
</div>
</div>
);
};
export default CollapsablePanel;
现在的效果:
使用react-use-measure可以非常方便的获取DOM对象的真实高度和在浏览器中的位置,在项目中灵活运用可以提高开发效率。
实现箭头图标旋转动画
箭头图标的旋转和内容区域的实现类似,只需要将其标签改成animated.div
,并将useSpring生成的样式对象绑定即可。
生成箭头ICON旋转动画style对象:
const toggleWrapperAnimatedStyle = useSpring({
transform: isCollapsed ? "rotate(0deg)" : "rotate(180deg)",
});
svg对象外套一个div,并绑定动画样式:
import React, { useState } from "react";
import { useSpring, animated } from "react-spring";
import useMeasure from 'react-use-measure'
import "./style.css";
const CollapsablePanel = () => {
const [isCollapsed, setIsCollapsed] = useState(true);
const [ref, bounds] = useMeasure();
const togglePanel = () => {
setIsCollapsed((prevState) => !prevState);
};
const panelContentAnimatedStyle = useSpring({
height: isCollapsed ? 0 : bounds.height,
});
const toggleWrapperAnimatedStyle = useSpring({
transform: isCollapsed ? "rotate(0deg)" : "rotate(180deg)",
});
return (
<div className="wrapper">
<div className="pannel" onClick={togglePanel}>
<div className="heading">
<span>Flower Collapse</span>
<animated.div style={toggleWrapperAnimatedStyle}>
<svg width="20px"
height="25px" viewBox="0 0 1024 1024"
style={{ color: '#6495ed' }}><path d="M64 351c0-8 3-16 9-22.2 12.3-12.7 32.6-13.1
45.3-0.8l394.1 380.5L905.7 328c12.7-12.3 33-12 45.3 0.7s12 33-0.7 45.3L534.7
776c-12.4 12-32.1 12-44.5 0L73.8 374c-6.5-6.3-9.8-14.6-9.8-23z" p-id="1705">
</path></svg>
</animated.div>
</div>
<animated.div
style={panelContentAnimatedStyle}
className="content"
>
<div ref={ref} className="contentInner" >
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam
nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam
erat, sed diam voluptua.
</div>
</animated.div>
</div>
</div>
);
};
export default CollapsablePanel;
可以看到如下效果:
总结
动手撸组件系列
第一篇文章选择讲Collapse组件的实现,是因为这个组件的实现简单而且富有趣味,读着可以体会到写组件的乐趣。一个简单的组件在实现的时候也有可能遇到问题,像如何获取content区域中的高度,就很有代表性。当前React及Vue的生态都异常繁荣,开发者在实现具体需求的时候要能够灵活运用这些开源库,以开发出简洁且功能强大的组件。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。