使用 React-DnD 打造簡易低代碼平臺
前言
2016年起,低代碼概念開始在國內(nèi)興起,當(dāng)年該行業(yè)總共有 10 起融資事件,之后低代碼行業(yè)融資筆數(shù)整體呈上升趨勢,并在2020年增長至14起,其中億元以上融資有13起。


從融資輪次分布上看,2016年天使輪、種子輪、A輪和B輪融資占比為50%,而到2020年,其占比則達(dá)到78.6%,相比2016年上升了28.6%。這可以說明低代碼市場整體仍處于發(fā)展初期 。
2021 年很多公司,不管大小,都開始開發(fā)低代碼平臺。低代碼即無需代碼或只需要通過少量代碼,通過“拖拽”的方式即可快速生成應(yīng)用程序。那么對于開發(fā)者而言,我們應(yīng)該如何入手開發(fā)呢?
“拖拽”實(shí)現(xiàn)
關(guān)鍵詞就是“拖拽”,其實(shí)“拖拽”的交互方式早在 Jquery 時(shí)代就有,關(guān)于拖拽在前端實(shí)現(xiàn)主要分為 2 種
是以 jquery-ui[1] 為代表的 draggable 和 ?Droppable,其原理是通過鼠標(biāo)事件 mousedown、mousemove、mouseup 或者 觸摸事件 touchstart、touchmove、touchend,記錄開始位置和結(jié)束位置、以達(dá)到拖拽傳遞數(shù)據(jù)的效果。
是通過 ?HTML5 Drag and Drop API[2]
下面是簡單實(shí)現(xiàn)代碼
<script>
function?dragstart_handler(ev)?{
?//?A將目標(biāo)元素的?id?添加到數(shù)據(jù)傳輸對象
?ev.dataTransfer.setData("application/my-app",?ev.target.id);
?ev.dataTransfer.effectAllowed?=?"move";
}
function?dragover_handler(ev)?{
?ev.preventDefault();
?ev.dataTransfer.dropEffect?=?"move"
}
function?drop_handler(ev)?{
?ev.preventDefault();
?//?獲取目標(biāo)的?id?并將已移動(dòng)的元素添加到目標(biāo)的?DOM?中
?const?data?=?ev.dataTransfer.getData("application/my-app");
?ev.target.appendChild(document.getElementById(data));
}
script>
<p?id="p1"?draggable="true"?ondragstart="dragstart_handler(event)">This?element?is?draggable.p>
<div?id="target"?ondrop="drop_handler(event)"?ondragover="dragover_handler(event)">Drop?Zonediv>
更高級的功能是:Drop API 還支持直接從系統(tǒng)桌面直接拖拽文件到瀏覽器中,使用 DataTransfer.files [3]實(shí)現(xiàn)拖拽上傳。
React-dnd
React DnD[4] 是 React 和 Redux 核心作者 Dan Abramov 創(chuàng)造的一組 React 工具庫,可以幫助您構(gòu)建復(fù)雜的拖放接口,同時(shí)保持組件的解耦性。例如,React DnD 沒有提供一個(gè)排序組件,相反,它為您提供了所需的工具。
官方 demo
一起來看下簡單實(shí)現(xiàn)

首先需要在項(xiàng)目根節(jié)點(diǎn)設(shè)置拖拽實(shí)現(xiàn)方式
import?{?render?}?from?'react-dom'
import?Example?from?'./example'
import?{?DndProvider?}?from?'react-dnd'
import?{?HTML5Backend?}?from?'react-dnd-html5-backend'
function?App()?{
????return?(
??????<div?className="App">
????????<DndProvider?backend={HTML5Backend}>
??????????<Example?/>
????????DndProvider>
??????div>
????)
}
如果是手機(jī)端就要使用 react-dnd-touch-backend,因?yàn)?react-dnd-html5-backend不支持觸摸。
DragBox 的實(shí)現(xiàn)
import?{?useDrag?}?from?'react-dnd';
import?{?ItemTypes?}?from?'./ItemTypes';
const?style?=?{
????cursor:?'move'
};
export?const?Box?=?function?Box({?name?})?{
????const?[{?isDragging?},?drag]?=?useDrag(()?=>?({
????????type:?ItemTypes.BOX,
????????item:?{?name?},
????????end:?(item,?monitor)?=>?{
????????????const?dropResult?=?monitor.getDropResult();
????????????if?(item?&&?dropResult)?{
????????????????alert(`You?dropped?${item.name}?into?${dropResult.name}!`);
????????????}
????????},
????????collect:?(monitor)?=>?({
????????????isDragging:?monitor.isDragging(),
????????????handlerId:?monitor.getHandlerId(),
????????}),
????}));
????const?opacity?=?isDragging???0.4?:?1;
????return?(<div?ref={drag}?style={{?...style,?opacity?}}>{name}div>);
};
這里的 type就是一個(gè)字符串,用于約束“拖”和“放”組件的關(guān)系,如果字符串不一致就無法回調(diào)事件,主要是為了避免頁面中多個(gè)拖放的實(shí)例item就是拖動(dòng)時(shí)候傳遞的數(shù)據(jù)end是拖放結(jié)束后的回調(diào)collect用于獲得拖動(dòng)的狀態(tài),可以設(shè)置樣式
DropContainer 實(shí)現(xiàn)
import?{?useDrop?}?from?'react-dnd';
import?{?ItemTypes?}?from?'./ItemTypes';
const?style?=?{
????...
};
export?const?DropContainer?=?()?=>?{
????const?[{?canDrop,?isOver?},?drop]?=?useDrop(()?=>?({
????????accept:?ItemTypes.BOX,
????????drop:?()?=>?({?name:?'Dustbin'?}),
????????collect:?(monitor)?=>?({
????????????isOver:?monitor.isOver(),
????????????canDrop:?monitor.canDrop(),
????????}),
????}));
????const?isActive?=?canDrop?&&?isOver;
????let?backgroundColor?=?'#222';
????if?(isActive)?{
????????backgroundColor?=?'darkgreen';
????}
????else?if?(canDrop)?{
????????backgroundColor?=?'darkkhaki';
????}
????return?(<div?ref={drop}?role={'Dustbin'}?style={{?...style,?backgroundColor?}}>
???{isActive???'Release?to?drop'?:?'Drag?a?box?here'}
????????div>);
};
type與拖動(dòng)的 type 相同drop函數(shù)返回放置節(jié)點(diǎn)的數(shù)據(jù),返回?cái)?shù)據(jù)給 drag endcollect用于獲得拖動(dòng)狀態(tài)的狀態(tài),可以設(shè)置樣式
低代碼實(shí)現(xiàn)
回到我們的低代碼主題,我們來一起看下釘釘宜搭的頁面設(shè)計(jì)

主要分為3個(gè)區(qū)域:左側(cè)組件區(qū)、中間設(shè)計(jì)區(qū)、右側(cè)編輯區(qū)。如果只看左側(cè)組件區(qū)和中間的設(shè)計(jì)區(qū)是否跟 react-dnd 官方的 demo 很相似呢?
定義 JSON
接下來我們要:
定義可拖動(dòng)的組件類型 每個(gè)組件類型對應(yīng)的渲染組件 每個(gè)組件的屬性設(shè)置
先來定義幾個(gè)可拖動(dòng)的字段吧,比如最基本的數(shù)據(jù)類型,div、h1、 p 標(biāo)簽都是一個(gè)組件,那就我先定義出以下字段類型,
const?fields=?[
??{
????type:?'div',
????props:?{
??????className:?'',
????},
??},
??{
????type:?'h1',
????props:?{
??????className:?'text-3xl',
??????children:?'H1',
????},
??},
??{
????type:?'p',
????props:?{
??????className:?'',
??????children:?'段落111',
????},
??}
??...
]
針對這些拖動(dòng)字段,需要有渲染的組件,而針對div、h1、 p 這些就是標(biāo)簽本身,但是我們需要用 react 封裝成組件
const?previewFields?=?{
??div:?(props:?any)?=>?<div?{...props}?/>,
??h1:?(props:?any)?=>?<h1?{...props}?/>,
??p:?(props:?any)?=>?<p?{...props}?/>,
??...
}
右側(cè)邊界區(qū)域的可配置字段
const?editAreaFields?=?{
????div:?[
??????{
????????key:?'className',
????????name:?'樣式',
????????type:?'Text',
??????},
????],
????h1:?[
??????{
????????key:?'children',
????????name:?'內(nèi)容',
????????type:?'Text',
??????},
????],
????p:?[
??????{
????????key:?'children',
????????name:?'內(nèi)容',
????????type:?'Text',
??????},
??????{
????????key:?'className',
????????name:?'樣式',
????????type:?'Text',
??????},
????],
????...
}
上述字段代表 div 只能設(shè)置 className、h1 只能設(shè)置內(nèi)容、p 標(biāo)簽既能設(shè)置內(nèi)容,也可以設(shè)置 className。右側(cè)區(qū)域的也可以配置不同的組件,比如 Text 就渲染成最簡單的 Input。
嵌套拖動(dòng)
基本組件一般可以嵌套的,比如我現(xiàn)在想要拖動(dòng)出下圖的頁面效果

實(shí)際上我需要生成 JSON 樹,然后根據(jù) JSON 樹渲染出頁面。

當(dāng)每次拖動(dòng)的時(shí)候,可以生成一個(gè) uuid,然后使用深度優(yōu)先遍歷樹數(shù)據(jù)從根節(jié)點(diǎn)到葉子節(jié)點(diǎn)的由上至下的深度優(yōu)先遍歷樹數(shù)據(jù)。在放置的組件,然后操作數(shù)據(jù)
export?const?traverse?=?extends?{?children?:?T[]?}>(
??data:?T,
??fn:?(param:?T)?=>?boolean
)?=>?{
??if?(fn(data)?===?false)?{
????return?false
??}
??if?(data?&&?data.children)?{
????for?(let?i?=?data.children.length?-?1;?i?>=?0;?i--)?{
??????if?(!traverse(data.children[i],?fn))?return?false
????}
??}
??return?true
}
豐富組件
還可以使用開源組件,集成到低代碼中,我們只需要定義右側(cè)編輯區(qū)域和左側(cè)字段數(shù)據(jù),比如現(xiàn)在集成 @ant-design/charts[5]
以柱狀圖為例,我們定義下拖動(dòng)的字段數(shù)據(jù)
{
type:?'Column',
module:?'@ant-design/charts',
h:?102,
displayName:?'柱狀圖組件',
props:?{
??xField:?'name',
??yField:?'value',
??data:?[
????{
??????name:?'A',
??????value:?20,
????},
????{
??????name:?'B',
??????value:?60,
????},
????{
??????name:?'C',
??????value:?20,
????},
??],
},
渲染 直接可以使用import { Column } from '@ant-design/charts';
props 增加默認(rèn)數(shù)據(jù)就可以直接渲染出漂亮的柱狀圖了。

然后增加一個(gè)數(shù)據(jù)編輯的組件,最后的效果如下圖

生成代碼
有了 JSON 樹,就可以生成想要的視圖代碼。組件類型 + props + 子組件的數(shù)據(jù),
每個(gè)節(jié)點(diǎn)的代碼就是這段字符串拼接而成。
<${sub.type}${props}>${children}${sub.type}>
而 props 也可以拼接成 key=value 的形式。遍歷數(shù)據(jù)要 從葉子節(jié)點(diǎn)到根節(jié)點(diǎn)的由下而上的深度優(yōu)先遍歷樹數(shù)據(jù)。
代碼格式化
我們可以使用 prettier 來格式化代碼,下面代碼是將格式化代碼的邏輯放到一個(gè) webWork 中。
importScripts('https://unpkg.com/[email protected]/standalone.js');
importScripts('https://unpkg.com/[email protected]/parser-babel.js');
self.addEventListener(
??'message',
??function?(e)?{
????self.postMessage(
??????prettier.format(e.data,?{
????????parser:?'babel',
????????plugins:?prettierPlugins,
??????})
????);
??},
??false
);
預(yù)覽
代碼有了,接下來就可以渲染頁面進(jìn)行預(yù)覽了,對于預(yù)覽,顯然是使用iframe,iframe除了src屬性外,HTML5還新增了一個(gè)屬性srcdoc,用來渲染一段HTML代碼到iframe里
iframeRef.value.contentWindow.document.write(htmlStr)
效果
拖拽一個(gè)表格 和一個(gè)柱狀圖

查看代碼

最后附上 github 和預(yù)覽地址
?? 倉庫地址:?github.com[6] ?? 預(yù)覽地址:?low-code.runjs.cool[7]
小結(jié)
本地記錄一個(gè)簡易低代碼的實(shí)現(xiàn)方式,簡單概括為 拖拽 -> JSON Tree——> 頁面
但想要真正生產(chǎn)可用還有很長的路要走,比如
組件數(shù)據(jù)綁定和聯(lián)動(dòng) 隨著組件數(shù)量的增加需要將組件服務(wù)化,動(dòng)態(tài)部署 組件開發(fā)者的成本與維護(hù)者的上手成本權(quán)衡 組件模板化 頁面部署投產(chǎn)等
以上任意一點(diǎn)都可能投入較高的成本,個(gè)人認(rèn)為目前低代碼,成本比較低且可以投產(chǎn)的方式有
1、類似?mall-cook[8] H5搭建

2、類似 json-editor[9] 表單搭建

?本文對低代碼搭建的思考和討論可能還不夠完整, 歡迎討論和補(bǔ)充。?希望這篇文章對大家有所幫助,也可以參考我往期的文章或者在評論區(qū)交流你的想法和心得,歡迎一起探索前端。
參考資料
jquery-ui: https://jqueryui.com/draggable/
[2]HTML5 Drag and Drop API: https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API
[3]DataTransfer.files : https://developer.mozilla.org/zh-CN/docs/Web/API/DataTransfer/files
[4]React DnD: https://react-dnd.github.io/react-dnd/about
[5]@ant-design/charts: https://charts.ant.design/zh/docs/manual/getting-started
[6]github.com: https://github.com/maqi1520/react-antd-low-code
[7]low-code.runjs.cool: https://low-code.runjs.cool/
[8]mall-cook: https://github.com/wangyuan389/mall-cook
[9]json-editor: https://github.com/json-editor/json-editor
The End
點(diǎn)個(gè)?「在看」,讓更多的人也能看到這篇文章;
關(guān)注公眾號?「前端Sharing」?,我們持續(xù)分享 Javascript 熱門框架和前端工程師進(jìn)階內(nèi)容;
