动手撸组件系列|动手撸组件系列 —— 1. 使用React实现一个Collapse组件

写组件的能力是衡量前端工程师水平的重要指标,不管是基础组件还是业务组件。
笔者在空闲时间也喜欢写组件,为了帮助初学者上手写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 (Flower CollapseLorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.); }; 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( );

创建完成后,使用yarn start命令启动应用,就能看到绘制好的Collapse组件外观:
动手撸组件系列|动手撸组件系列 —— 1. 使用React实现一个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, });

接着把内容区域的标签名从改为即可(和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 (Flower CollapseLorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.); }; export default CollapsablePanel;

点击标题栏可以看到如下效果:
动手撸组件系列|动手撸组件系列 —— 1. 使用React实现一个Collapse组件
文章图片

等等~~ 高度是固定的吗?
显然不是!用户在使用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 (Flower CollapseLorem 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.); }; export default CollapsablePanel;

【动手撸组件系列|动手撸组件系列 —— 1. 使用React实现一个Collapse组件】现在的效果:
动手撸组件系列|动手撸组件系列 —— 1. 使用React实现一个Collapse组件
文章图片

使用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 (Flower CollapseLorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.); }; export default CollapsablePanel;

可以看到如下效果:
动手撸组件系列|动手撸组件系列 —— 1. 使用React实现一个Collapse组件
文章图片

总结
动手撸组件系列第一篇文章选择讲Collapse组件的实现,是因为这个组件的实现简单而且富有趣味,读着可以体会到写组件的乐趣。一个简单的组件在实现的时候也有可能遇到问题,像如何获取content区域中的高度,就很有代表性。当前React及Vue的生态都异常繁荣,开发者在实现具体需求的时候要能够灵活运用这些开源库,以开发出简洁且功能强大的组件。
Coollapse.jsx源代码
style.css源代码

    推荐阅读