新聞中心
一、寫在前面
低代碼編輯器作為一種能夠極大地提升開發(fā)效率的PaaS軟件,近些年來一直收到各大公司以及各路投資方的追捧。而對(duì)于我們前端開發(fā)者來說,編輯器也是為數(shù)不多的擁有較深前端技術(shù)深度的開發(fā)場景。

創(chuàng)新互聯(lián)是一家以網(wǎng)站設(shè)計(jì)建設(shè),重慶小程序開發(fā)、網(wǎng)站開發(fā)設(shè)計(jì),網(wǎng)絡(luò)軟件產(chǎn)品開發(fā),企業(yè)互聯(lián)網(wǎng)推廣服務(wù)為主的民營科技公司。主要業(yè)務(wù)涵蓋:為客戶提供網(wǎng)站策劃、網(wǎng)站設(shè)計(jì)、網(wǎng)站開發(fā)、域名與空間、網(wǎng)站優(yōu)化排名、買友情鏈接等服務(wù)領(lǐng)域。憑借建站老客戶口碑做市場,建設(shè)網(wǎng)站時(shí),根據(jù)市場搜索規(guī)律和搜索引擎的排名收錄規(guī)律編程,全力為建站客戶設(shè)計(jì)制作排名好的網(wǎng)站,深受老客戶認(rèn)可和贊譽(yù)。
通過本篇文章,你能夠?qū)W會(huì)如何基于 React 技術(shù)棧搭建一個(gè)最簡單的低代碼編輯器,以及部分重點(diǎn)功能的實(shí)現(xiàn)方式。
本文示例代碼已經(jīng)開源到 GitHub 上,有需要的朋友們可以自?。篽ttps://github.com/shadowings-zy/mini-editor 。簡單預(yù)覽下這個(gè)編輯器的 demo:
二、編輯器功能拆分
先上一張?jiān)蛨D:
請(qǐng)?zhí)砑訄D片描述
對(duì)于大部分低代碼編輯器來說,都是由「組件區(qū)」、「畫布區(qū)」、「屬性編輯區(qū)」這三個(gè)部分組成的。
- 組件區(qū)負(fù)責(zé)展示可拖拽的組件和組件間的層級(jí)關(guān)系。
- 畫布區(qū)負(fù)責(zé)渲染拖拽的組件,并對(duì)其進(jìn)行可視化展示。
- 屬性編輯區(qū)負(fù)責(zé)編輯選中組件的屬性。
而針對(duì)這三個(gè)區(qū)域所負(fù)責(zé)的職責(zé),我們能夠很容易地設(shè)計(jì)出這三個(gè)區(qū)域所需要實(shí)現(xiàn)的功能:
- 對(duì)于組件區(qū),我們需要保證組件是可拖拽的,并且組件能與畫布區(qū)交互
- 對(duì)于畫布區(qū),我們需要首先抽象出一個(gè)用于展現(xiàn)「畫布區(qū)有哪些組件」的數(shù)據(jù)格式,然后畫布區(qū)能夠根據(jù)這個(gè)數(shù)據(jù)格式渲染對(duì)應(yīng)的組件。其次,我們還需要實(shí)現(xiàn)拖拽后的組件與畫布的交互,以及選中組件后與屬性編輯區(qū)的交互。
- 對(duì)于屬性編輯區(qū),我們需要處理屬性變更后,與對(duì)應(yīng)組件的聯(lián)動(dòng)邏輯。
三、編輯器數(shù)據(jù)格式的定義
編輯器底層的數(shù)據(jù)格式是開發(fā)低代碼編輯器的重中之重,畫布區(qū)會(huì)根據(jù)這個(gè)數(shù)據(jù)去渲染畫布,而組件的拖拽,組件屬性的配置,實(shí)際上都是對(duì)這個(gè)數(shù)據(jù)的更改。
而回到我們編輯器本身,我們可以使用 json 格式的數(shù)據(jù)來抽象編輯器畫布的內(nèi)容,像下面這樣:
{
"projectId": "xxx", // 項(xiàng)目 ID
"projectName": "xxx", // 項(xiàng)目名稱
"author": "xxx", // 項(xiàng)目作者
"data": [
// 畫布組件配置
{
"id": "xxx", // 組件 ID
"type": "text", // 組件類型
"data": "xxxxxx", // 文字內(nèi)容
"color": "#000000", // 文字顏色
"size": "12px", // 文字大小
"width": "100px", // 容器寬度
"height": "100px", // 容器高度
"left": "100px", // 容器左邊距
"top": "100px" // 容器上邊距
},
{
"id": "xxx", // 組件 ID
"type": "image", // 組件類型
"data": "http://xxxxxxx", // 圖片 url
"width": "100px", // 容器寬度
"height": "100px", // 容器高度
"left": "100px", // 容器左邊距
"top": "100px" // 容器上邊距
},
{
"id": "xxx", // 組件 ID
"type": "video", // 組件類型
"data": "http://xxxxxxx", // 視頻 url
"width": "100px", // 容器寬度
"height": "100px", // 容器高度
"left": "100px", // 容器左邊距
"top": "100px" // 容器上邊距
}
]
}定義好數(shù)據(jù)結(jié)構(gòu)后,「組件屬性編輯」、「拖拽新增組件」實(shí)際上都是對(duì)這個(gè) json 數(shù)據(jù)中的 data 字段進(jìn)行增刪改,而畫布區(qū)也會(huì)使用這個(gè)字段進(jìn)行畫布內(nèi)組件的渲染。
四、項(xiàng)目代碼結(jié)構(gòu)
整體代碼結(jié)構(gòu)如下:
.
├── package
│ ├── client # 前端頁面
│ │ ├── build # webpack 打包配置
│ │ │ ├── webpack.base.js
│ │ │ ├── webpack.dev.js
│ │ │ └── webpack.prod.js
│ │ ├── components # 前端組件
│ │ │ ├── textComponent # 組件區(qū)中的「文字組件」
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.css
│ │ │ └── textPanel # 「文字組件」對(duì)應(yīng)的屬性編輯組件
│ │ │ ├── index.tsx
│ │ │ └── style.css
│ │ ├── constants # 一些常量
│ │ │ └── index.ts
│ │ ├── index.html
│ │ ├── index.tsx
│ │ ├── pages # 前端頁面
│ │ │ ├── app # 根組件
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.css
│ │ │ ├── drawPanel # 畫布區(qū)
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.css
│ │ │ ├── leftPanel # 左側(cè)組件區(qū)
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.css
│ │ │ └── rightPanel # 右側(cè)屬性編輯區(qū)
│ │ │ ├── index.tsx
│ │ │ └── style.css
│ │ ├── style.css
│ │ └── tsconfig.json
│ └── server # 后端代碼
│ ├── app.ts # 后端邏輯
│ ├── config # 后端配置
│ │ ├── dev.ts
│ │ ├── index.ts
│ │ └── prod.ts
│ ├── constants.ts # 一些常量
│ └── tsconfig.json
├── package.json
├── pnpm-lock.yaml
└── tsconfig.json
五、重點(diǎn)邏輯的實(shí)現(xiàn)
在梳理重點(diǎn)邏輯之前,我們得先梳理下我們的編輯器組件需要維護(hù)哪些數(shù)據(jù)。
- 首先是編輯器數(shù)據(jù),畫布需要根據(jù)編輯器數(shù)據(jù)來渲染內(nèi)容,而添加組件和屬性修改本質(zhì)上都是對(duì)這個(gè)數(shù)據(jù)的改動(dòng)。
- 其次是右側(cè)面板的類型,編輯不同組件要有不同類型的編輯項(xiàng)。
- 另外還有當(dāng)前選中的組件 id,右側(cè)屬性面板的改動(dòng)都是對(duì)當(dāng)前組件 id 生效的。
所以我們將這些數(shù)據(jù)維護(hù)在根組件下,并用 props 將它們傳遞給其他子組件,代碼如下:
import DrawPanel from "../drawPanel"; // 畫布
import LeftPanel from "../leftPanel"; // 左側(cè)組件面板
import RightPanel from "../rightPanel"; // 右側(cè)屬性編輯面板
export default function App() {
const [drawPanelData, setDrawPanelData] = useState([]); // 編輯器數(shù)據(jù)
const [rightPanelType, setRightPanelType] = useState(RIGHT_PANEL_TYPE.NONE); // 右側(cè)屬性面板類型
const [rightPanelElementId, setRightPanelElementId] = useState(""); // 右側(cè)屬性面板編輯的 id
return (
data={drawPanelData}
setData={setDrawPanelData}
setRightPanelType={setRightPanelType}
setRightPanelElementId={setRightPanelElementId}
>
type={rightPanelType}
data={drawPanelData}
elementId={rightPanelElementId}
setDrawPanelData={setDrawPanelData}
>
);
}
定義好這些數(shù)據(jù)之后,我們就來講解一下重點(diǎn)邏輯的實(shí)現(xiàn)。
1、畫布渲染?
首先我們來看一下畫布渲染邏輯的實(shí)現(xiàn):
這里先要將畫布區(qū)的布局調(diào)整為??position: relative???,然后將每個(gè)組件的布局設(shè)置為??position: absolute???,這樣我們就能根據(jù)編輯器數(shù)據(jù)中的??left???和??top??屬性來定位組件在畫布的位置了。
然后就是遍歷編輯器數(shù)據(jù),將對(duì)應(yīng)的組件渲染到畫布上。具體代碼如下:
// package/client/pages/drawPanel/index.tsx
interface IDrawPanelProps {
data: any; // 將編輯器數(shù)據(jù)作為 props 傳入組件中
}
export default function DrawPanel(props: IDrawPanelProps) {
const { data } = props;
const generateContent = () => {
const output = [];
// 遍歷編輯器數(shù)據(jù)并渲染畫布
for (const item of data) {
if (item.type === COMPONENT_TYPE.TEXT) {
output.push(
key={item.id}
style={{
color: item.color,
fontSize: item.size,
width: item.width,
height: item.height,
left: item.left,
top: item.top,
position: "absolute",
backgroundColor: "#bbbbbb",
}}
>
{item.data}
);
}
}
return output;
};
return (
className="draw-panel"
ref={drop}
style={{
position: "relative",
}}
>
{generateContent()}
);
}
2、屬性聯(lián)動(dòng)?
接著,為了實(shí)現(xiàn)屬性聯(lián)動(dòng),我們需要實(shí)現(xiàn)以下幾件事:
- 為畫布上的組件添加點(diǎn)擊事件,使其能夠在點(diǎn)擊時(shí)設(shè)置右側(cè)屬性編輯面板的內(nèi)容。
- 在右側(cè)屬性編輯面板編輯組件屬性時(shí),要能對(duì)編輯器數(shù)據(jù)中對(duì)應(yīng)目標(biāo)組件的數(shù)據(jù)進(jìn)行改動(dòng),然后畫布區(qū)根據(jù)新的編輯器數(shù)據(jù)進(jìn)行渲染。
為了實(shí)現(xiàn)第一點(diǎn),我們就要在畫布組件中為每一個(gè)渲染出的組件添加點(diǎn)擊事件了,使用 props 傳入的??setRightPanelType???和??setRightPanelElementId??來設(shè)置對(duì)應(yīng)選中的元素,代碼如下:
// package/client/pages/drawPanel/index.tsx
export default function DrawPanel(props: IDrawPanelProps) {
const { data, setRightPanelType, setRightPanelElementId } = props;
const generateContent = () => {
const output = [];
for (const item of data) {
if (item.type === COMPONENT_TYPE.TEXT) {
output.push(
key={item.id}
style={{
color: item.color,
fontSize: item.size,
width: item.width,
height: item.height,
left: item.left,
top: item.top,
position: 'absolute',
backgroundColor: '#bbbbbb'
}}
+ // 在這里添加點(diǎn)擊事件
+ onClick={() => {
+ setRightPanelType(RIGHT_PANEL_TYPE.TEXT);
+ setRightPanelElementId(item.id);
+ }}
>
{item.data}
);
}
}
return output;
};
// ... 其他邏輯
}
而為了實(shí)現(xiàn)右側(cè)面板能夠?qū)崟r(shí)編輯數(shù)據(jù),我們首先需要根據(jù)傳入的??elementId???,遍歷編輯器數(shù)據(jù),拿到要修改的項(xiàng),然后獲取對(duì)應(yīng)的屬性改動(dòng)值,最后使用??setDrawPanelData??進(jìn)行修改。具體代碼如下:
interface IRigthPanelProps {
type: RIGHT_PANEL_TYPE;
data: any;
elementId: string;
setDrawPanelData: Function;
}
export default function RightPanel(props: IRigthPanelProps) {
const { type, data, elementId, setDrawPanelData } = props;
const findCurrentElement = (id: string) => {
for (const item of data) {
if (item.id === id) {
return item;
}
}
return undefined;
};
const findCurrentElementAndChangeData = (
id: string,
key: string,
changedData: any
) => {
for (let item of data) {
if (item.id === id) {
item[key] = changedData;
}
}
setDrawPanelData([...data]);
};
const generateRightPanel = () => {
if (type === RIGHT_PANEL_TYPE.NONE) {
return 未選中元素;
} else if (type === RIGHT_PANEL_TYPE.TEXT) {
const elementData = findCurrentElement(elementId);
const inputDomObject = [];
return (
文字元素
文字內(nèi)容:
defaultValue={elementData.data}
ref={(element) => {
inputDomObject[0] = element;
}}
type="text"
>
文字顏色:
defaultValue={elementData.color}
ref={(element) => {
inputDomObject[1] = element;
}}
type="text"
>
文字大小:
defaultValue={elementData.size}
ref={(element) => {
inputDomObject[2] = element;
}}
type="text"
>
width:
defaultValue={elementData.width}
ref={(element) => {
inputDomObject[3] = element;
}}
type="text"
>
height:
defaultValue={elementData.height}
ref={(element) => {
inputDomObject[4] = element;
}}
type="text"
>
top:
defaultValue={elementData.top}
ref={(element) => {
inputDomObject[5] = element;
}}
type="text"
>
left:
defaultValue={elementData.left}
ref={(element) => {
inputDomObject[6] = element;
}}
type="text"
>
);
}
};
return {generateRightPanel()}
網(wǎng)站名稱:從零開始實(shí)現(xiàn)一個(gè)簡單的低代碼編輯器
本文路徑:http://fisionsoft.com.cn/article/dheehdc.html


咨詢
建站咨詢
