关注订阅号「豆皮范儿」,回复“加群”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系统中核心的就是模板+组件库。目前组件没有版本的概念,硬编码到代码中,须要拆分出来异步引用,另外也须要作好对所在容器宽高作自适应。