Implemente um editor simples de baixo código do zero

1. Escreva na frente

Como um software PaaS que pode melhorar muito a eficiência do desenvolvimento, os editores de código baixo têm sido procurados por grandes empresas e investidores nos últimos anos. Para nossos desenvolvedores de front-end, o editor também é um dos poucos cenários de desenvolvimento com grande profundidade de tecnologia de front-end.

Por meio deste artigo, você pode aprender como criar o editor de código baixo mais simples com base na pilha de tecnologia React e como implementar algumas funções importantes.

O código de exemplo deste artigo foi aberto no GitHub, e amigos que precisam podem obtê-lo por conta própria: https://github.com/shadowings-zy/mini-editor

Basta visualizar a demonstração deste editor:
Adicione uma descrição da imagem

2. Diretório

  • Divisão da função do editor
  • Definição do formato de dados do editor
  • estrutura do código do projeto
  • Implementação da lógica principal (renderização da tela, vinculação de atributos, arrastar componentes)
  • Interaja com o plano de fundo
  • Pontos que também podem ser otimizados

3. Divisão da função do editor

Vamos começar com um diagrama de protótipo:
Adicione uma descrição da imagem
Para a maioria dos editores de código baixo, eles são compostos de três partes: "área de componentes", "área de tela" e "área de edição de propriedades".

  • A área de componentes é responsável por exibir os componentes arrastáveis ​​e o relacionamento hierárquico entre os componentes.
  • A área da tela é responsável por renderizar os componentes arrastados e exibi-los visualmente.
  • A área de edição de atributos é responsável por editar os atributos do componente selecionado.

Com base nas responsabilidades dessas três áreas, podemos facilmente projetar as funções que essas três áreas precisam realizar:

  • Para a área do componente, precisamos garantir que os componentes sejam arrastáveis ​​e que possam interagir com a área da tela
  • Para a área da tela, precisamos primeiro abstrair um formato de dados para exibir "quais componentes estão na área da tela" e, em seguida, a área da tela pode renderizar os componentes correspondentes de acordo com esse formato de dados. Em segundo lugar, também precisamos realizar a interação entre o componente arrastado e a tela, e a interação com a área de edição de atributos após a seleção do componente.
  • Para a área de edição de atributos, precisamos lidar com a lógica de ligação com o componente correspondente após a alteração do atributo.

4. Definição do formato de dados do editor

O formato dos dados na parte inferior do editor é a coisa mais importante para desenvolver um editor de baixo código. A área da tela renderizará a tela de acordo com esses dados, e arrastar e soltar os componentes e a configuração das propriedades dos componentes são, na verdade, alterações a estes dados.

E voltando ao nosso editor propriamente dito, podemos usar dados no formato json para abstrair o conteúdo da tela do editor, assim:

{
    
    
  "projectId": "xxx", // 项目 ID
  "projectName": "xxx", // 项目名称
  "author": "xxx", // 项目作者
  "data": [
    // 画布组件配置
    {
    
    
      "id": "xxx", // 组件 ID
      "type": "text", // 组件类型
      "data": "xxxxxx", // 文字内容
      "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" // 容器上边距
    }
  ]
}

Depois de definir a estrutura de dados, "edição de propriedade do componente" e "arrastar e soltar para adicionar componentes" são, na verdade, adicionando, excluindo e modificando o campo de dados nos dados json, e a área de tela também usará esse campo para renderizar os componentes em a tela.

5. Estrutura do código do projeto

A estrutura geral do código é a seguinte:

.
├── package
│   ├── client # 前端页面
│   │   ├── build # webpack 打包配置
│   │   │   ├── webpack.base.js
│   │   │   ├── webpack.dev.js
│   │   │   └── webpack.prod.js
│   │   ├── components # 前端组件
│   │   │   ├── textComponent # 组件区中的「文字组件」
│   │   │   │   ├── index.tsx
│   │   │   │   └── style.css
│   │   │   └── textPanel # 「文字组件」对应的属性编辑组件
│   │   │       ├── index.tsx
│   │   │       └── style.css
│   │   ├── constants # 一些常量
│   │   │   └── index.ts
│   │   ├── index.html
│   │   ├── index.tsx
│   │   ├── pages # 前端页面
│   │   │   ├── app # 根组件
│   │   │   │   ├── index.tsx
│   │   │   │   └── style.css
│   │   │   ├── drawPanel # 画布区
│   │   │   │   ├── index.tsx
│   │   │   │   └── style.css
│   │   │   ├── leftPanel # 左侧组件区
│   │   │   │   ├── index.tsx
│   │   │   │   └── style.css
│   │   │   └── rightPanel # 右侧属性编辑区
│   │   │       ├── 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

6. Realização da Lógica Chave

Antes de classificar a lógica principal, temos que classificar quais dados nossos componentes do editor precisam manter.

  • O primeiro são os dados do editor. A tela precisa renderizar o conteúdo de acordo com os dados do editor, e adicionar componentes e modificar propriedades são essencialmente alterações nesses dados.
  • O segundo é o tipo do painel direito.A edição de diferentes componentes requer diferentes tipos de itens de edição.
  • Além disso, existe o ID do componente selecionado no momento e as alterações no painel de propriedades à direita terão efeito no ID do componente atual.

Então mantemos esses dados sob o componente raiz e passamos para outros subcomponentes com props, o código é o seguinte:

import DrawPanel from "../drawPanel"; // 画布
import LeftPanel from "../leftPanel"; // 左侧组件面板
import RightPanel from "../rightPanel"; // 右侧属性编辑面板

export default function App() {
    
    
  const [drawPanelData, setDrawPanelData] = useState([]); // 编辑器数据
  const [rightPanelType, setRightPanelType] = useState(RIGHT_PANEL_TYPE.NONE); // 右侧属性面板类型
  const [rightPanelElementId, setRightPanelElementId] = useState(""); // 右侧属性面板编辑的 id

  return (
    <div className="flex-row-space-between app">
      <LeftPanel data={
    
    drawPanelData}></LeftPanel>
      <DrawPanel
        data={
    
    drawPanelData}
        setData={
    
    setDrawPanelData}
        setRightPanelType={
    
    setRightPanelType}
        setRightPanelElementId={
    
    setRightPanelElementId}
      ></DrawPanel>
      <RightPanel
        type={
    
    rightPanelType}
        data={
    
    drawPanelData}
        elementId={
    
    rightPanelElementId}
        setDrawPanelData={
    
    setDrawPanelData}
      ></RightPanel>
    </div>
  );
}

Depois de definir esses dados, vamos explicar a implementação da lógica da chave.

6-1. Renderização da tela

Primeiro, vamos dar uma olhada na implementação da lógica de renderização da tela:

Aqui, precisamos ajustar o layout da área da tela para position: relative, e então definir o layout de cada componente para , para que possamos localizar a posição do componente na tela position: absolutede acordo com leftas propriedades e .top

Em seguida, ele percorre os dados do editor e renderiza os componentes correspondentes na tela.

O código específico é o seguinte:

// package/client/pages/drawPanel/index.tsx

interface IDrawPanelProps {
    
    
  data: any; // 将编辑器数据作为 props 传入组件中
}

export default function DrawPanel(props: IDrawPanelProps) {
    
    
  const {
    
     data } = props;

  const generateContent = () => {
    
    
    const output = [];
    // 遍历编辑器数据并渲染画布
    for (const item of data) {
    
    
      if (item.type === COMPONENT_TYPE.TEXT) {
    
    
        output.push(
          <div
            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}
          </div>
        );
      }
    }

    return output;
  };

  return (
    <div
      className="draw-panel"
      ref={
    
    drop}
      style={
    
    {
    
    
        position: "relative",
      }}
    >
      {
    
    generateContent()}
    </div>
  );
}

6-2. Ligação de atributos

Em seguida, para realizar a vinculação de atributos, precisamos implementar o seguinte:

1. Adicione um evento de clique ao componente na tela para que ele possa definir o conteúdo do painel de edição de propriedade à direita quando for clicado.

2. Ao editar as propriedades do componente no painel de edição de propriedades à direita, é necessário modificar os dados correspondentes ao componente de destino nos dados do editor e, em seguida, a área da tela é renderizada de acordo com os novos dados do editor.

Para atingir o primeiro ponto, precisamos adicionar um evento click a cada componente renderizado no componente canvas, e usar setRightPanelTypee setRightPanelElementIdpara definir o elemento selecionado correspondente, o código é o seguinte:

// 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(
          <div
            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'
            }}

+           // 在这里添加点击事件
+           onClick={
    
    () => {
    
    
+             setRightPanelType(RIGHT_PANEL_TYPE.TEXT);
+               setRightPanelElementId(item.id);
+           }}
          >
            {
    
    item.data}
          </div>
        );
      }
    }

    return output;
  };

  // ... 其他逻辑
}

elementIdPara perceber que o painel direito pode editar dados em tempo real, primeiro precisamos percorrer os setDrawPanelDatadados de acordo com a entrada , obter o item a ser modificado e, em seguida, obter o valor de alteração do atributo correspondente e, finalmente, usá-lo para modificá-lo . O código específico é o seguinte:

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 <div>未选中元素</div>;
    } else if (type === RIGHT_PANEL_TYPE.TEXT) {
    
    
      const elementData = findCurrentElement(elementId);
      const inputDomObject = [];

      return (
        <div key={
    
    elementId}>
          <div>文字元素</div>
          <br />
          <div className="flex-row-space-between text-config-item">
            <div>文字内容:</div>
            <input
              defaultValue={
    
    elementData.data}
              ref={
    
    (element) => {
    
    
                inputDomObject[0] = element;
              }}
              type="text"
            ></input>
          </div>
          <div className="flex-row-space-between text-config-item">
            <div>文字颜色:</div>
            <input
              defaultValue={
    
    elementData.color}
              ref={
    
    (element) => {
    
    
                inputDomObject[1] = element;
              }}
              type="text"
            ></input>
          </div>
          <div className="flex-row-space-between text-config-item">
            <div>文字大小:</div>
            <input
              defaultValue={
    
    elementData.size}
              ref={
    
    (element) => {
    
    
                inputDomObject[2] = element;
              }}
              type="text"
            ></input>
          </div>
          <div className="flex-row-space-between text-config-item">
            <div>width:</div>
            <input
              defaultValue={
    
    elementData.width}
              ref={
    
    (element) => {
    
    
                inputDomObject[3] = element;
              }}
              type="text"
            ></input>
          </div>
          <div className="flex-row-space-between text-config-item">
            <div>height:</div>
            <input
              defaultValue={
    
    elementData.height}
              ref={
    
    (element) => {
    
    
                inputDomObject[4] = element;
              }}
              type="text"
            ></input>
          </div>
          <div className="flex-row-space-between text-config-item">
            <div>top:</div>
            <input
              defaultValue={
    
    elementData.top}
              ref={
    
    (element) => {
    
    
                inputDomObject[5] = element;
              }}
              type="text"
            ></input>
          </div>
          <div className="flex-row-space-between text-config-item">
            <div>left:</div>
            <input
              defaultValue={
    
    elementData.left}
              ref={
    
    (element) => {
    
    
                inputDomObject[6] = element;
              }}
              type="text"
            ></input>
          </div>
          <br />
          <button
            onClick={
    
    () => {
    
    
              findCurrentElementAndChangeData(
                elementId,
                "data",
                inputDomObject[0].value
              );
              findCurrentElementAndChangeData(
                elementId,
                "color",
                inputDomObject[1].value
              );
              findCurrentElementAndChangeData(
                elementId,
                "size",
                inputDomObject[2].value
              );
              findCurrentElementAndChangeData(
                elementId,
                "width",
                inputDomObject[3].value
              );
              findCurrentElementAndChangeData(
                elementId,
                "height",
                inputDomObject[4].value
              );
              findCurrentElementAndChangeData(
                elementId,
                "top",
                inputDomObject[5].value
              );
              findCurrentElementAndChangeData(
                elementId,
                "left",
                inputDomObject[6].value
              );
            }}
          >
            确定
          </button>
        </div>
      );
    }
  };

  return <div className="right-panel">{
    
    generateRightPanel()}</div>;
}

6-3. Arraste e solte os componentes

Por fim, chega-se ao mais importante, como implementar arrastar e soltar, que é usado aqui react-dnd, reacta biblioteca oficial de arrastar e soltar, a referência do documento aqui: https://react-dnd.github.io/react-dnd/ sobre

react-dndEm , drage dropdois tipos de componentes são definidos, obviamente, os componentes que precisam ser arrastados no painel esquerdo são dragcomponentes e a tela é dropo componente.

Para os componentes que precisam ser arrastados à esquerda, usamos react-dndos useDragganchos fornecidos para torná-los arrastáveis, o código é o seguinte:

// package/client/components/textComponent/index.tsx

export default function TextComponent() {
    
    
  const [_, drag] = useDrag(() => ({
    
    
    type: COMPONENT_TYPE.TEXT,
  }));

  return (
    <div className="text-component" ref={
    
    drag}>
      文字组件
    </div>
  );
}

Para a tela, usamos useDropganchos e getClientOffsetfunções para obter a posição arrastada, calcular leftos topvalores e dos novos componentes e, em seguida, usar setDataos dados do editor de configurações, o código é o seguinte:

export default function DrawPanel(props: IDrawPanelProps) {
    
    
  const {
    
     data, setRightPanelType, setRightPanelElementId, setData } = props;

  const [, drop] = useDrop(() => ({
    
    
    accept: COMPONENT_TYPE.TEXT,
    drop: (_, monitor) => {
    
    
      const {
    
     x, y } = monitor.getClientOffset();
      const currentX = x - 310;
      const currentY = y - 20;

      setData([
        ...data,
        {
    
    
          id: `text-${
      
      data.length + 1}`,
          type: "text",
          data: "我是新建的文字",
          color: "#000000",
          size: "12px",
          width: "100px",
          height: "20px",
          left: `${
      
      currentX}px`,
          top: `${
      
      currentY}px`,
        },
      ]);
    },
  }));

  // ... 其他逻辑
}

Sete, interaja com o fundo

Depois de implementada a lógica do editor, a lógica de interação com o background é bem simples. Basta iniciar uma requisição ao background e salvar/recuperar os dados json do editor. Aqui, a lógica de salvar os dados para o background é simplesmente implementado. O código é o seguinte:

import axios from "axios";

export default function LeftPanel(props: ILeftPanelProps) {
    
    
  const {
    
     data } = props;

  return (
    <div className="left-panel">
      <div className="component-list">
        <TextComponent></TextComponent>
      </div>
      <button
        className="save-button"
        onClick={
    
    () => {
    
    
          console.log("save:", data);
          axios
            .post("/api/save", {
    
     drawPanelData: data })
            .then((res) => {
    
    
              console.log("res:", res);
            })
            .catch((err) => {
    
    
              console.log("err:", err);
            });
        }}
      >
        保存到后台
      </button>
    </div>
  );
}

Depois que o plano de fundo recebe os dados, eles podem ser armazenados no banco de dados.

import Koa from "koa";
import Router from "koa-router";
import koaStatic from "koa-static";
import koaBody from "koa-body";
import {
    
     config } from "./config";
import {
    
     PORT } from "./constants";

const app = new Koa();

app.use(koaBody());

const router = new Router();

router.get("/api", async (ctx, next) => {
    
    
  ctx.body = {
    
     message: "Hello World" };
  await next();
});

router.post("/api/save", async (ctx, next) => {
    
    
  console.log("save:", ctx.request.body);
  // ...储存到数据库
  ctx.body = {
    
    
    message: "Save data successful",
    receivedData: ctx.request.body,
  };
  await next();
});

app.use(router.routes());
app.use(router.allowedMethods());

app.use(koaStatic(config.staticFilePath));

app.listen(PORT, () => {
    
    
  console.log(`Server listening on port ${
      
      PORT}`);
});

8. Pontos que podem ser explorados em profundidade

Através dos passos acima podemos implementar o editor low-code mais simples, mas ainda existem muitos pontos técnicos que podem ser explorados a fundo, vou listar alguns deles e as soluções abaixo.

8-1 Como implementar o aninhamento de componentes?

Para o aninhamento de componentes, precisamos modificar a lógica de operação dos dados do editor (ou seja, dados jsonde formato ), da "inserção de array" original para uma operação para um determinado nível. Ao mesmo tempo, a lógica da travessia do componente também deve ser mudado.

8-2 Um componente de edição de atributo de nível superior pode ser abstraído?

A maioria dos editores na indústria realmente faz essa camada de abstração. Para diferentes componentes de edição de atributo, eles serão usados ​​para schemadescrever os itens editáveis ​​desse componente de edição e os dados que podem ser alterados correspondentes a esse item de edição. O componente de edição de atributo é realmente usado para consumir isso schemapara renderização.

8-3 Como enviar um arquivo relativamente grande como um vídeo para o servidor?

Isso envolve o upload de arquivos em partes e armazenamento combinado de back-end. Você pode ver este projeto que implementei antes: https://github.com/shadowings-zy/easy-file-uploader

8-4 Uma solução de gerenciamento de dados global melhor?

Como a implementação é relativamente simples, este artigo coloca todos os dados no componente raiz e os passa como propsparâmetros , mas, na verdade, um componente editor de código baixo complexo tem uma hierarquia profunda e propsnão é realista usá-lo. Nesse caso, usar globais reduxcomo Uma biblioteca para gerenciamento de dados seria mais conveniente.

Acho que você gosta

Origin blog.csdn.net/u011748319/article/details/124799496
Recomendado
Clasificación