基于JS實(shí)現(xiàn)一個(gè)可拖拽的容器布局組件
1. 前言
某一天,產(chǎn)品經(jīng)理給我提了這樣一個(gè)需求:產(chǎn)品概覽頁(yè)是一個(gè)三列布局的結(jié)構(gòu),我希望用戶能夠自己拖動(dòng)列與列之間的分割線,實(shí)現(xiàn)每列的寬度自定義,國(guó)際站用戶就經(jīng)常有這樣的需求。效果類似這樣:

就這?簡(jiǎn)單啊,不就是拖拽嗎?使用開源拖拽庫(kù),回調(diào)里面給相關(guān)容器設(shè)置一下寬度即可,幾行代碼就搞定了。...不對(duì),這是新同學(xué)才應(yīng)該有的想法,但我是一個(gè)老前端啊,后來(lái)我又想了一下,如果我實(shí)現(xiàn)了上面的功能,那兩列布局、三列布局、不管幾列布局都應(yīng)該可以拖拽啊,那頁(yè)面左邊的菜單,右邊彈出的抽屜也可以讓用戶拖拽啊,嗯...那就做成一個(gè)組件吧,讓我們來(lái)優(yōu)雅的實(shí)現(xiàn)它。
2. 組件分析
我們先分析一下,不管是兩列布局、三列布局、菜單、抽屜,最后拖拽的其實(shí)都是一根線,所以首先我們需要封裝一個(gè)拖拽線條的組件,有了這個(gè)組件,再實(shí)現(xiàn)任何布局拖拽寬度自定義的功能就簡(jiǎn)單很多了:

使用開源庫(kù)還是自己實(shí)現(xiàn),也是我考慮的一個(gè)問題,我最終還是選擇了自己實(shí)現(xiàn),原因主要有兩點(diǎn):第一是現(xiàn)在的開源得三方包體積都比較大,我們的業(yè)務(wù)組件是項(xiàng)目必須引用的資源,資源當(dāng)然是越小越好;第二是我們這個(gè)功能比較簡(jiǎn)單,自己實(shí)現(xiàn)代碼可控,還可以實(shí)用一些新特性讓性能做到最優(yōu)。
3. DragLine
DragLine組件主要包括哪些能力呢?
- 內(nèi)置拖拽能力,可配置拖拽開始和結(jié)束的回調(diào)函數(shù)。
- 內(nèi)置提示信息,可配置是否在第一次渲染時(shí)默認(rèn)進(jìn)行“可拖拽”的信息提示。
廢話不多說,直接上拖拽線條組件DragLine的代碼:
// DragLine.js
import React, { useEffect, useRef, useState } from 'react';
import { Button, Tooltip } from 'antd';
import './index.scss';
const DragLine = ((props) => {
const {
gap = 16,
onMouseMove,
onMouseUp,
style = {},
tipKey,
defaultShowTip = false,
...rest
} = props;
const [visible, setVisible] = useState(defaultShowTip);
const ref = useRef(null);
const eventRef = useRef({});
const closeNavTips = () => {
localStorage.setItem(tipKey, 'true'); // 設(shè)置標(biāo)記
setVisible(false); // 關(guān)閉彈窗
};
// 拖拽結(jié)束
const handleMouseUp = (e) => {
document.body.classList.remove('dragging');
onMouseUp && onMouseUp(e, ref.current);
document.removeEventListener('mousemove', eventRef.current.mouseMoveHandler, false);
document.removeEventListener('mouseup', eventRef.current.mouseUpHandler, false);
};
// 拖拽中
const handleMouseMove = (e) => {
onMouseMove && onMouseMove(e, ref.current);
};
// 開始拖拽
const handleMouseDown = () => {
closeNavTips();// 關(guān)閉拖拽提示框
document.body.classList.add('dragging');
eventRef.current.mouseMoveHandler = (e) => handleMouseMove(e);
eventRef.current.mouseUpHandler = (e) => handleMouseUp(e);
document.addEventListener('mousemove', eventRef.current.mouseMoveHandler, false);
document.addEventListener('mouseup', eventRef.current.mouseUpHandler, false);
};
const line = (
<div
ref={ref}
style={{
'--drag-gap': `${gap}px`,
...style,
}}
className={`drag-line ${visible ? 'active' : ''}`}
onMouseDown={handleMouseDown}
{...rest}
/>
);
return visible ? (
<Tooltip
open
placement="rightTop"
title={(
<div>
<div style={{ marginBottom: 4 }}>拖動(dòng)這根線試試~</div>
<Button size="small" onClick={closeNavTips}>關(guān)閉</Button>
</div>
)}
>
{line}
</Tooltip>
) : line;
});
export default DragLine;
對(duì)應(yīng)的css代碼如下:
/* index.scss */
.drag-line {
width: 2px;
margin: 0 calc((var(--drag-gap, 16px) - 2px) / 2);
background: transparent;
cursor: col-resize;
&.active, &:hover {
background: blue;
}
}
.dragging {
user-select: none; // 內(nèi)容不可選擇
}
上述代碼,拷貝后可以直接運(yùn)行,我簡(jiǎn)單說明其中幾點(diǎn):
- js文件53行,使用到了css變量,對(duì)應(yīng)css文件第4行,并通過
calc函數(shù)可以實(shí)現(xiàn)很多復(fù)雜功能。 - js文件42行,拖拽時(shí)給body增加類名,對(duì)應(yīng)css文件第14行,設(shè)置拖拽時(shí)body內(nèi)容不可選中,不然用戶會(huì)在拖拽時(shí)無(wú)意選中很多內(nèi)容,從而造成困惑。
- 組件代碼非常簡(jiǎn)單,并且內(nèi)部已經(jīng)封裝好了拖拽能力,以及彈出的提示框,只是拋出了幾個(gè)簡(jiǎn)單的API給業(yè)務(wù)方使用即可,我們還可以根據(jù)實(shí)際需求進(jìn)一部分封裝,比如線條的寬度、提示的內(nèi)容和位置等等。
4. DragContainer
有了 DragLine 這個(gè)基礎(chǔ)組件后,我們就可以很容易的去擴(kuò)展任何需要拖拽的上層組件了,比如我們來(lái)實(shí)現(xiàn)一個(gè)可拖拽的多列布局容器組件,直接上DragContainer組件的源碼:
// DragContainer.js
import React, { useRef } from 'react';
import DragLine from '../DragLine';
import classnames from 'classnames';
import './index.scss';
const DragContainer = (props) => {
const {
className,
sceneKey,
minChildWidth = 150,
contentList = [],
gap = 16,
} = props;
const cls = classnames('drag-container', className);
const ref = useRef(null);
// 拖拽結(jié)束時(shí),保存寬度信息
const onMouseUp = () => {
const widthList = contentList.map((_, i) => {
const child = ref.current.querySelector(`.item${i}`);
return `${child?.offsetWidth}px`;
});
localStorage.setItem(sceneKey, widthList.join('#'));
};
const onMouseMove = (event, node) => {
const index = parseInt(node.getAttribute('data-index'));
const leftElement = ref.current.querySelector(`.item${index}`);
const rightElement = ref.current.querySelector(`.item${index + 1}`);
// 拖動(dòng)距離 = 分割線的位置 - 鼠標(biāo)的位置
const dragOffset = node.getBoundingClientRect().left - event.clientX;
const newLeftChildWidth = leftElement.offsetWidth - dragOffset;
const newRightChildWidth = rightElement.offsetWidth + dragOffset;
if (newLeftChildWidth >= minChildWidth && newRightChildWidth >= minChildWidth) {
ref.current.style.setProperty(`--drag-childWidth-${sceneKey}-${index}`, `${newLeftChildWidth}px`);
ref.current.style.setProperty(`--drag-childWidth-${sceneKey}-${index + 1}`, `${newRightChildWidth}px`);
}
};
const contentData = [];
const localWidthList = localStorage.getItem(sceneKey)?.split('#') || []; // 獲取本地已經(jīng)保存的寬度信息
contentList.forEach((d, i) => {
contentData.push(
<div
key={`${sceneKey}_${i}`}
className={`container-item item${i}`}
style={{ flexBasis: `var(--drag-childWidth-${sceneKey}-${i}, ${localWidthList[i]})` }}
>ublnpf9mb
</div>,
);
if (i < contentList.length - 1) {
contentData.push(
<DragLine
key={`${sceneKey}_dragline_${i}`}
onMouseMove={onMouseMove}
onMouseUp={onMouseUp}
tipKey="draggableContainerFlag"
data-index={i}
defaultShowTip={i === 0}
gap={gap}
/>,
);
}
});
return (
<div ref={ref} className={cls}>
{contentData}
</div>
);
};
export default DragContainer;
對(duì)應(yīng)樣式文件如下:
/* index.scss */
.drag-container {
display: flex;
align-items: stretch;
width: 100%;
.container-item {
height: 100%;
overflow: hidden;
flex: 1; // 同比例放大縮小
}
}
DragContainer組件的實(shí)現(xiàn)邏輯也比較簡(jiǎn)單,基本思路如下:
- 根據(jù)傳入的contentList進(jìn)行一個(gè)循環(huán),如果不是最后一個(gè)child,則多渲染一個(gè)
DragLine,用以拖拽。 - 在拖拽線條的回調(diào)函數(shù)里,進(jìn)行一個(gè)拖拽偏移和左右子元素新寬度的計(jì)算,再設(shè)置到css變量中,從而實(shí)現(xiàn)拖拽寬度實(shí)時(shí)變化的效果。并且代碼中沒有用到任何
React State,不需要重復(fù)渲染整個(gè)組件,改變寬度直接使用css實(shí)現(xiàn),性能也比較好。 - css文件第10行,對(duì)flex布局的子元素設(shè)置
flex: 1,意思是當(dāng)我們拖動(dòng)瀏覽器窗口大小時(shí),子元素的寬度會(huì)同比例放大縮小,就能實(shí)現(xiàn)寬度自適應(yīng)了,但這里有個(gè)前提是,子元素寬度不要寫死,而是配合js文件第53行的flexBasis屬性一起使用。 - 上述代碼拷貝后也是可以直接運(yùn)行的,需要的同學(xué)可以直接試試。
5. 使用效果
我們?cè)跇I(yè)務(wù)代碼中使用DragContainer組件寫個(gè)例子,使用簡(jiǎn)單,效果完美:
<DragContainer
sceneKey="overview-page"
contentList={[
<Card>111</Card>,
<Card>222</Card>,
<Card>333</Card>,
]}
/>

6. 總結(jié)
其實(shí)本文我最想要表達(dá)的是,當(dāng)我們接到一個(gè)需求之后,先學(xué)會(huì)分析和過濾,如果是特定的業(yè)務(wù)需求,實(shí)現(xiàn)即可,如果是通用類需求,就要慢慢學(xué)會(huì)從組件開發(fā)的角度去思考,是否能夠舉一反三,通過組件開發(fā)去覆蓋解決更多的場(chǎng)景和問題。另外是在功能的實(shí)現(xiàn)方面,主要總結(jié)以下幾點(diǎn):
- 要能夠通過對(duì)比選擇最合適自己的技術(shù),比如簡(jiǎn)單的拖拽功能完全可以使用原生js來(lái)做,而不是引入一個(gè)超大的三方包。
- 容器寬度的改變可以直接修改css屬性,而不是使用React狀態(tài),減少不必要的重復(fù)渲染。
- css variable技術(shù),是打通js和css的一種手段。
- flex布局相關(guān)屬性的熟練使用,可以以更優(yōu)的方案來(lái)解決一些布局問題。
到此這篇關(guān)于基于JS實(shí)現(xiàn)一個(gè)可拖拽的容器布局組件的文章就介紹到這了,更多相關(guān)JS可拖拽容器布局組件內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
js cavans實(shí)現(xiàn)靜態(tài)滾動(dòng)彈幕
這篇文章主要為大家詳細(xì)介紹了js cavans實(shí)現(xiàn)靜態(tài)滾動(dòng)彈幕,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-05-05
使用cookie繞過驗(yàn)證碼登錄的實(shí)現(xiàn)代碼
本文通過實(shí)例代碼給大家詳細(xì)介紹了使用cookie繞過驗(yàn)證碼登錄的實(shí)現(xiàn)方法,需要的朋友參考下吧2017-10-10
javascript通過class來(lái)獲取元素實(shí)現(xiàn)代碼
javascript獲取元素有很多的方法,本文簡(jiǎn)單的介紹下通過class獲取元素的實(shí)現(xiàn)代碼,感興趣的朋友可以參考下,希望本文知識(shí)點(diǎn)可以幫助到你2013-02-02

