關注訂閱號「豆皮範兒」,回覆「加羣」javascript
加入咱們一塊兒學習,day day uphtml
Hi~ 豆皮粉們!剛過完年,去年年終總結有小夥伴沒有寫嗎?有寫新的一年的規劃嗎?2021又準備立哪些Flag?前端
這次就請你們來讀讀由字節跳動的「米蘭的小鐵匠」 精心製做的《大盤開發從入門到所見即所得》,見識一下大盤開發中須要用的知識,讓你不懂的知識+1。html5
本文主要內容java
- 拖拽的原理
- 常見拖拽組件庫比較
- React-DnD快速上手
- Re-resizable快速上手
- 如何實現一個最簡單的拖拽大盤系統
最近給咱們的後臺系統作了一個所見即所得的大盤編輯器,很有收穫,寫篇文章作個全面的回顧react
對一個DOM元素而言,完整的拖拽流程分爲兩部分,即 拖動 + 放置git
讓一個元素支持拖動是一件很是容易作到的事情,咱們只須要在對應的HTML結點新增一個draggable="true"
的屬性便可,另外,超連接和圖像都是默承認拖動的。github
真正麻煩的是放置部分,咱們須要監聽ondragstart
、ondragenter
、ondragover
、ondragleave
等等各階段發生在元素上的拖動事件,最後還須要處理ondrop
事件完成最終的放置,咱們須要作好數據的傳遞,可放置區域的識別、最終位置的處理,頁面的更新等等一系列細小繁瑣的工做。web
所幸的是,已經有成熟的庫來幫助咱們完善這些細節了,讓咱們只須要關注於渲染邏輯便可。數據庫
下面列出了常見的React拖拽相關的庫:
React DnD 是由Redux做者Dan Abramov主導開發,也是很是老牌的React拖拽工具庫,提供了對底層的拖拽的一層封裝。
React-Beautiful-DnD 是由Alassian團隊(沒錯,就是開發Jira的團隊)貢獻的React拖拽工具庫。相比於React-DnD,提供了更高層級功能的封裝,如動畫、虛擬列表、移動端等功能。也是Github上Star最多的React 拖拽庫
React-Grid-Layout是由一家比特幣交易公司BitMex開源的,可謂柵格佈局模式下集成最好的框架庫,支持放大縮小,自動佈局,在AWS控制檯與Grafana中已經使用了此框架,對初學者很是友好。
因爲這裏我並不想把本身的命運交給比特幣公司,更想從偏底層來實現本身的一整套拖拽邏輯,故此選用了React-DnD庫來完成頁面拖動功能的開發。
React-dnd中,包含四個核心概念:backend
,monitor
,drag
,drop
。
下面是一個最簡單最基本的例子:
import { HTML5Backend } from 'react-dnd-html5-backend'
import { DndProvider, useDrag, useDrop } from 'react-dnd'
function Drag() {
const [collectedProps, drag] = useDrag({
item: { values, type: 'KEY' }
})
return (
<div ref={drag}>Drag</div>
)
}
function Drop() {
const [collectedProps, drop] = useDrop({
accept: 'KEY'
})
return (
<div ref={drop}>Drop Area</div>
)
}
export default function Demo() {
return (
<DndProvider backend={HTML5Backend}> <Drag /> <Drop /> </DndProvider>
)
}
複製代碼
此處的backend,能夠理解爲拖拽背後的實現的邏輯,此處主要是用來區分PC端和移動端不一樣的事件監聽和處理方式,若是是運行在PC端的,使用react-dnd-html5-backend
,不然就使用react-dnd-touch-backend
,注意DndProvider
必定是在Drag和Drop的最外層使用的。
monitor一眼看上去其實並很差理解,可是確實沒有更貼切的單詞了。monitor是監控整個拖動事件的總狀態數據,主要分爲sourceMonitor和targetMonitor,分別表明Drag和Drop元素當前的狀態數據,如偏移距離、是否浮於上層等等。咱們在使用useDrag和useDrop的時候,能夠經過對應的monitor數據進行狀態斷定或者預置切換等等豐富功能。
drag即容許拖動的元素(source),咱們經過useDrag生成的ref指向給了某一個DIV,此DIV便會被設置draggable=true
的屬性,同時拖動的全部事件都會被咱們監聽到。使用方法能夠參考上面例子。
const [collectedProps, drag] = useDrag({item, canDrag, collect})
複製代碼
useDrag返回的數組一共有三個元素,咱們只說前兩個:
collectedProps: 這實際上是React-DnD一個很精妙的設計,組件在拖動的時候,此變量便表明着須要監聽的數據
drag: 即拖動元素的Ref引用,賦給對應的DOM元素便可
複製代碼
useDrag的函數參數也不少,這裏只挑重要的說一下:
item: 必填,即包含的數據對象,必須字段type,與drop對象對應,只有同一個type值的才能被放置進去
canDrag: 選填,(monitor) => boolean,表示是否可拖拽,這在區分編輯與只讀模式很是有用
collect: 選填,(monitor) => object,經過此方法返回的值能夠從上述的collectedProps中取到, 經過使用monitor判斷狀態,咱們能夠返回如opacity、hightlighted等屬性用來給拖動元素添加樣式
複製代碼
drop便可以拖動到的元素(target),它的返回數組有兩個元素,並且與useDrag返回值做用幾乎一致
const [collectedProps, drop] = useDrop({ accept, hover, drop, collect })
複製代碼
其中參數和返回值以下:
collectedProps: 同上,也是collect函數返回的object
drop: 即放置元素的Ref引用,賦給對應的DOM元素便可
複製代碼
useDrop的參數也不少,咱們也挑重點的說明一下:
accept: 必填,支持字符串或者字符串數組,對應於drag的type值,一樣的值纔可被拖入此元素中
hover: 選填,(item, monitor) => void,item即拖動到此drop上drag對象的值,經過用於展現滑上後的預覽效果
drop: 選填,(item, monitor) => void,同上,此事件在鼠標放開後觸發
collect: 選填,(monitor) => object,做用同上,也能夠用來表達drag進來和離開事件
複製代碼
至此全部的react-dnd基本概念已經介紹完了,正所謂「九層之臺起於累土,千里之行始於足下」,頁面上的全部交互都是基於這些最基本的功能實現的,也許你仍然以爲很抽象,不妨參考下官網的Demo其中Sandbox的代碼例子來學習一下,擠需體驗十番鍾,裏造會幹我同樣,愛象節款工具!
上面說完了拖拽,下面該說一下拉伸了。
拉伸是可經過在CSS屬性中指定resize
來支持拉伸,好比常見的textarea就是默認內置了此屬性,可是瀏覽器並未像drag同樣提供resize專門的API,故大部分庫都是經過監聽mousedown
,mousemove
,mouseup
這種有些hack的方式完成的。
re-resizable 也是React體系下支持拉伸的庫,這個庫入門很是簡單,只看官方文檔就能很快理解。
咱們能夠像表單組件同樣給它設置value(也就是size)和onChange(也就是onRisizeStop)便可完成拉伸的功能,比較麻煩的是enable若是指定了則八個方向都需指定一遍。
值得一提的是如何去作寬高的輔助吸附,簡單點可使用grid
來設置步長,若是要作定製化的對齊就麻煩了,這裏分享一個思路,咱們能夠在onResize或onResizeStop的時候,經過參數咱們能夠獲取偏移位置,此時能夠對偏移位置進行計算後四捨五入,即可保證按比例變化。
若是想作相似Photoshop(不是PS)或者CAD那種橫軸縱軸吸附的,能夠參考document.elementFromPoint(x,y)
方法,經過不斷加步長迭代的方式應該能夠找到最近的子元素並獲取對應的寬高。
我把整個拖拽系統分紅四部分:
下面的TYPE即表示useDrag中的type值,ACCEPT即表示useDrop中的accept的值。
TYPE="Container"
拖拽源容器即全部可供用戶拖拽到畫布上的容器佈局,全部的組件應當被放置到容器內進行佈局上的管理,若是組件能實現良好的佈局管理其實也能夠不須要此容器。
TYPE="Widget"
即實際業務上須要的展現組件,這部分是支持二次開發的,且用了Form-Render 支持以配置項的方式生成組件配置表單,組件只須要關注業務邏輯,配置項會自動注入進來。
TYPE="PaintContainer" ACCEPT=["Container", "PaintContainer"]
當把拖拽源拖入畫布後,即生成一個畫布容器區域,也能夠不用一個新的TYPE,這樣作主要是便於快速區分是從拖拽源過來的或是畫布上模塊的移動,若是想讓一個DOM同時支持Drag & Drop,能夠這樣作:
const ref = useRef();
const [,drop] = useDrop({});
const [,drag] = useDrag({});
drop(drag(ref));
return <div ref={ref}> Both Can Drag & Drop </div>
複製代碼
TYPE="PaintWidget" ACCEPT=["Widget", "PaintWidget"]
這裏也能夠用兩個不一樣的TYPE來區分,區分從拖拽源進來的仍是從畫布上別的地方拖進來的,一個是把數據填充進去,一個是交換兩個位置的下標。
畫布區域使用一個JS數組來維護,數組元素大體結構以下:
{
uuid: string; // 惟一標識區塊的id
width,height... // 定位與尺寸屬性
children: { // 裏面的子展現組件
uuid: string; // 惟一標識展現組件的id
span: number; // 展現組件佔寬度
widgetId: string; // 具體是哪個展現組件,渲染時會取組件列表中獲取並渲染
config: object; // 個性化配置項值
}[]
}
複製代碼
這裏不得不讚美一下React的 Render(data) => View 模式作這種畫布實在太合適了,每次只要修改了數據結構,React就會自動根據數據結構渲染出畫布裏具體的內容,少操了不少心。
1. 如何作日期數據補0?
這廣泛發生在作折線圖的時候,DB的數據並非天天都有,特別是在畫多條折線圖的時候:
[
{ data: '2020-09-01', type: 'A', count: 5 },
{ data: '2020-09-03', type: 'A', count: 15 },
{ data: '2020-09-03', type: 'B', count: 10 },
{ data: '2020-09-06', type: 'C', count: 20 },
]
複製代碼
上面的數據,缺乏了9月2日和9月4日,9月5日的數據,若是不把空缺的時間填上去,那橫軸間隔就會很奇怪。
而且由於是多條折線,每一個日期都須要每種type對應的數據,否則會出現折線斷掉的狀況。
補0的方法無非三種思路:
設查詢的時間範圍長度爲N,返回的記錄爲responseData數組
,總種類數爲M,分享一個O(NM)時間複雜度的方法(由於最終數組長度就是N*M,因此應該仍是蠻高效的)
第一步:用dayjs工具
生成從查詢起始時間到終止時間的時間序列數組dateList
,元素爲日期string
第二步:生成空的結果數組resultList
,參考Echarts規範,這個數組的格式爲{ type: value[] },type就是狀態值,value的下標是日期的下標,值是count數據
第三步:下標指針i指向dateList
第0
個元素,下標指針j指向responseData
第0
個元素
第四步:先不比較,遍歷M全部狀態,給resultList[Enum(M)][i]
賦值resultList[Enum(M)][i] || 0
第五步:比較dateList[i]
和responseData[j]
對應的日期是否同樣,若是同樣,則跳轉到第六步,不然到第七步
第六步:賦值resultList[type][i]
爲responseData[j].count
,這裏的type是responseData[j].type
,而後j++
,由於還要在結果中找尋同一個日期下其餘數據,接着返回第四步
第七步:說明結果中不存在此日期下數據,由於第四步中已經作了默認值賦值,因此直接i++
,而後返回第四步
第八步:當i
超過dateList
的長度後,終止循環便可
這畢竟是兩個星期作出來的東西,還有不少實現並不完善的地方:
1. 拖拽交互
拖拽交互若是想要增長動效,預覽等等效果,須要增長不少細節上的判斷
2.佈局
目前強制行優先佈局,強制四平八整,可能須要支持列方向上的佈局
3.組件庫建設
CMS系統中核心的就是模板+組件庫。目前組件沒有版本的概念,硬編碼到代碼中,須要拆分出來異步引用,另外也須要作好對所在容器寬高作自適應。