基于JS實現(xiàn)一個可拖拽的容器布局組件
1. 前言
某一天,產(chǎn)品經(jīng)理給我提了這樣一個需求:產(chǎn)品概覽頁是一個三列布局的結(jié)構(gòu),我希望用戶能夠自己拖動列與列之間的分割線,實現(xiàn)每列的寬度自定義,國際站用戶就經(jīng)常有這樣的需求。效果類似這樣:
就這?簡單啊,不就是拖拽嗎?使用開源拖拽庫,回調(diào)里面給相關(guān)容器設(shè)置一下寬度即可,幾行代碼就搞定了。...不對,這是新同學(xué)才應(yīng)該有的想法,但我是一個老前端啊,后來我又想了一下,如果我實現(xiàn)了上面的功能,那兩列布局、三列布局、不管幾列布局都應(yīng)該可以拖拽啊,那頁面左邊的菜單,右邊彈出的抽屜也可以讓用戶拖拽啊,嗯...那就做成一個組件吧,讓我們來優(yōu)雅的實現(xiàn)它。
2. 組件分析
我們先分析一下,不管是兩列布局、三列布局、菜單、抽屜,最后拖拽的其實都是一根線,所以首先我們需要封裝一個拖拽線條的組件,有了這個組件,再實現(xiàn)任何布局拖拽寬度自定義的功能就簡單很多了:
使用開源庫還是自己實現(xiàn),也是我考慮的一個問題,我最終還是選擇了自己實現(xiàn),原因主要有兩點:第一是現(xiàn)在的開源得三方包體積都比較大,我們的業(yè)務(wù)組件是項目必須引用的資源,資源當(dāng)然是越小越好;第二是我們這個功能比較簡單,自己實現(xiàn)代碼可控,還可以實用一些新特性讓性能做到最優(yōu)。
3. DragLine
DragLine
組件主要包括哪些能力呢?
- 內(nèi)置拖拽能力,可配置拖拽開始和結(jié)束的回調(diào)函數(shù)。
- 內(nèi)置提示信息,可配置是否在第一次渲染時默認(rè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 }}>拖動這根線試試~</div> <Button size="small" onClick={closeNavTips}>關(guān)閉</Button> </div> )} > {line} </Tooltip> ) : line; }); export default DragLine;
對應(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)容不可選擇 }
上述代碼,拷貝后可以直接運行,我簡單說明其中幾點:
- js文件53行,使用到了css變量,對應(yīng)css文件第4行,并通過
calc
函數(shù)可以實現(xiàn)很多復(fù)雜功能。 - js文件42行,拖拽時給body增加類名,對應(yīng)css文件第14行,設(shè)置拖拽時body內(nèi)容不可選中,不然用戶會在拖拽時無意選中很多內(nèi)容,從而造成困惑。
- 組件代碼非常簡單,并且內(nèi)部已經(jīng)封裝好了拖拽能力,以及彈出的提示框,只是拋出了幾個簡單的API給業(yè)務(wù)方使用即可,我們還可以根據(jù)實際需求進一部分封裝,比如線條的寬度、提示的內(nèi)容和位置等等。
4. DragContainer
有了 DragLine
這個基礎(chǔ)組件后,我們就可以很容易的去擴展任何需要拖拽的上層組件了,比如我們來實現(xiàn)一個可拖拽的多列布局容器組件,直接上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é)束時,保存寬度信息 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}`); // 拖動距離 = 分割線的位置 - 鼠標(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;
對應(yīng)樣式文件如下:
/* index.scss */ .drag-container { display: flex; align-items: stretch; width: 100%; .container-item { height: 100%; overflow: hidden; flex: 1; // 同比例放大縮小 } }
DragContainer
組件的實現(xiàn)邏輯也比較簡單,基本思路如下:
- 根據(jù)傳入的contentList進行一個循環(huán),如果不是最后一個child,則多渲染一個
DragLine
,用以拖拽。 - 在拖拽線條的回調(diào)函數(shù)里,進行一個拖拽偏移和左右子元素新寬度的計算,再設(shè)置到css變量中,從而實現(xiàn)拖拽寬度實時變化的效果。并且代碼中沒有用到任何
React State
,不需要重復(fù)渲染整個組件,改變寬度直接使用css實現(xiàn),性能也比較好。 - css文件第10行,對flex布局的子元素設(shè)置
flex: 1
,意思是當(dāng)我們拖動瀏覽器窗口大小時,子元素的寬度會同比例放大縮小,就能實現(xiàn)寬度自適應(yīng)了,但這里有個前提是,子元素寬度不要寫死,而是配合js文件第53行的flexBasis
屬性一起使用。 - 上述代碼拷貝后也是可以直接運行的,需要的同學(xué)可以直接試試。
5. 使用效果
我們在業(yè)務(wù)代碼中使用DragContainer
組件寫個例子,使用簡單,效果完美:
<DragContainer sceneKey="overview-page" contentList={[ <Card>111</Card>, <Card>222</Card>, <Card>333</Card>, ]} />
6. 總結(jié)
其實本文我最想要表達(dá)的是,當(dāng)我們接到一個需求之后,先學(xué)會分析和過濾,如果是特定的業(yè)務(wù)需求,實現(xiàn)即可,如果是通用類需求,就要慢慢學(xué)會從組件開發(fā)的角度去思考,是否能夠舉一反三,通過組件開發(fā)去覆蓋解決更多的場景和問題。另外是在功能的實現(xiàn)方面,主要總結(jié)以下幾點:
- 要能夠通過對比選擇最合適自己的技術(shù),比如簡單的拖拽功能完全可以使用原生js來做,而不是引入一個超大的三方包。
- 容器寬度的改變可以直接修改css屬性,而不是使用React狀態(tài),減少不必要的重復(fù)渲染。
- css variable技術(shù),是打通js和css的一種手段。
- flex布局相關(guān)屬性的熟練使用,可以以更優(yōu)的方案來解決一些布局問題。
到此這篇關(guān)于基于JS實現(xiàn)一個可拖拽的容器布局組件的文章就介紹到這了,更多相關(guān)JS可拖拽容器布局組件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
javascript通過class來獲取元素實現(xiàn)代碼
javascript獲取元素有很多的方法,本文簡單的介紹下通過class獲取元素的實現(xiàn)代碼,感興趣的朋友可以參考下,希望本文知識點可以幫助到你2013-02-02