寫元件的能力是衡量前端工程師水平的重要指標,不管是基礎元件還是業務元件。
筆者在空閒時間也喜歡寫元件,為了幫助初學者上手寫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的生態都異常繁榮,開發者在實現具體需求的時候要能夠靈活運用這些開源庫,以開發出簡潔且功能強大的元件。