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