Implemente un editor de código bajo simple desde cero

1. Escribe al frente

Como un software PaaS que puede mejorar en gran medida la eficiencia del desarrollo, las principales empresas e inversores han buscado editores de código bajo en los últimos años. Para nuestros desarrolladores front-end, el editor también es uno de los pocos escenarios de desarrollo con una gran profundidad en la tecnología front-end.

A través de este artículo, puede aprender cómo crear el editor de código bajo más simple basado en la pila de tecnología React y cómo implementar algunas funciones clave.

El código de muestra de este artículo se ha abierto en GitHub, y los amigos que lo necesiten pueden obtenerlo por sí mismos: https://github.com/shadowings-zy/mini-editor

Simplemente obtenga una vista previa de la demostración de este editor:
Por favor agregue una descripción de la imagen

2. Directorio

  • División de la función del editor
  • Definición del formato de datos del editor
  • estructura del código del proyecto
  • Implementación de lógica clave (representación de lienzo, vinculación de atributos, arrastre de componentes)
  • Interactuar con el fondo
  • Puntos que también se pueden optimizar

3. División de funciones del editor

Comencemos con un diagrama prototipo:
Por favor agregue una descripción de la imagen
para la mayoría de los editores de código bajo, se componen de tres partes: "área de componentes", "área de lienzo" y "área de edición de propiedades".

  • El área de componentes es responsable de mostrar los componentes que se pueden arrastrar y la relación jerárquica entre los componentes.
  • El área del lienzo es responsable de representar los componentes arrastrados y mostrarlos visualmente.
  • El área de edición de atributos es responsable de editar los atributos del componente seleccionado.

Con base en las responsabilidades de estas tres áreas, podemos diseñar fácilmente las funciones que estas tres áreas deben cumplir:

  • Para el área de componentes, debemos asegurarnos de que los componentes se puedan arrastrar y los componentes puedan interactuar con el área del lienzo.
  • Para el área del lienzo, primero debemos abstraer un formato de datos para mostrar "qué componentes hay en el área del lienzo", y luego el área del lienzo puede representar los componentes correspondientes de acuerdo con este formato de datos. En segundo lugar, también debemos darnos cuenta de la interacción entre el componente arrastrado y el lienzo, y la interacción con el área de edición de atributos después de seleccionar el componente.
  • Para el área de edición de atributos, necesitamos manejar la lógica de vinculación con el componente correspondiente después de cambiar el atributo.

4. Definición del formato de datos del editor

El formato de datos en la parte inferior del editor es lo más importante para desarrollar un editor de código bajo. El área del lienzo representará el lienzo de acuerdo con estos datos, y el arrastrar y soltar componentes y la configuración de las propiedades de los componentes son en realidad cambios. a estos datos.

Y volviendo a nuestro propio editor, podemos usar datos en formato json para abstraer el contenido del lienzo del editor, así:

{
    
    
  "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" // 容器上边距
    }
  ]
}

Después de definir la estructura de datos, "edición de propiedades del componente" y "arrastrar y soltar para agregar componentes" en realidad agregan, eliminan y modifican el campo de datos en los datos json, y el área del lienzo también usará este campo para representar los componentes en el lienzo.

5. Estructura del código del proyecto

La estructura general del código es la siguiente:

.
├── 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. Realización de Key Logic

Antes de ordenar la lógica clave, debemos determinar qué datos deben mantener nuestros componentes de editor.

  • El primero son los datos del editor. El lienzo debe representar el contenido de acuerdo con los datos del editor, y agregar componentes y modificar propiedades son esencialmente cambios en estos datos.
  • El segundo es el tipo del panel derecho.Editar diferentes componentes requiere diferentes tipos de elementos de edición.
  • Además, está la identificación del componente seleccionado actualmente, y los cambios en el panel de propiedades de la derecha tendrán efecto en la identificación del componente actual.

Entonces mantenemos estos datos bajo el componente raíz y los pasamos a otros subcomponentes con props, el código es el siguiente:

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>
  );
}

Después de definir estos datos, expliquemos la implementación de la lógica clave.

6-1 Representación del lienzo

Primero, echemos un vistazo a la implementación de la lógica de representación del lienzo:

Aquí tenemos que ajustar el diseño del área del lienzo position: relativey luego establecer el diseño de cada componente en , para que podamos ubicar la posición del componente en el lienzo position: absolutede acuerdo con leftlas propiedades y .top

Luego, atraviesa los datos del editor y representa los componentes correspondientes en el lienzo.

El código específico es el siguiente:

// 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 Vinculación de atributos

A continuación, para realizar la vinculación de atributos, debemos implementar lo siguiente:

1. Agregue un evento de clic al componente en el lienzo para que pueda establecer el contenido del panel de edición de propiedades a la derecha cuando se hace clic en él.

2. Al editar las propiedades del componente en el panel de edición de propiedades de la derecha, es necesario poder modificar los datos correspondientes al componente de destino en los datos del editor, y luego el área del lienzo se representa de acuerdo con los nuevos datos del editor.

Para lograr el primer punto, debemos agregar un evento de clic a cada componente renderizado en el componente de lienzo, y usar setRightPanelTypey setRightPanelElementIdpara establecer el elemento seleccionado correspondiente, el código es el siguiente:

// 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 darnos cuenta de que el panel derecho puede editar datos en tiempo real, primero debemos recorrer los setDrawPanelDatadatos de acuerdo con la entrada , hacer que se modifique el elemento y luego obtener el valor de cambio de atributo correspondiente, y finalmente usar para modificarlo . El código específico es el siguiente:

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 Componentes de arrastrar y soltar

Finalmente, se trata de lo más importante, cómo implementar arrastrar y soltar, que se usa aquí react-dnd, reactla biblioteca oficial de arrastrar y soltar, la referencia del documento aquí: https://react-dnd.github.io/react-dnd/ acerca de

react-dndEn , dragy dropse definen dos tipos de componentes, obviamente, los componentes que deben arrastrarse en el panel izquierdo son dragcomponentes, y el lienzo es dropel componente.

Para los componentes que deben arrastrarse a la izquierda, usamos react-dndlos ganchos provistos useDragpara hacerlos arrastrables, el código es el siguiente:

// 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 el lienzo, usamos useDropganchos y getClientOffsetfunciones para obtener la posición arrastrada, calcular leftlos topvalores de los nuevos componentes y luego usar setDatalos datos del editor de configuración, el código es el siguiente:

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`,
        },
      ]);
    },
  }));

  // ... 其他逻辑
}

Siete, interactúa con el fondo.

Una vez que hemos implementado la lógica del editor, la lógica de interactuar con el fondo es bastante simple. Basta con iniciar una solicitud al fondo y guardar/recuperar los datos json del editor. Aquí, la lógica de guardar datos en el fondo se implementa simplemente.El código es el siguiente:

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>
  );
}

Una vez que el fondo recibe los datos, se pueden almacenar en la base de datos.

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. Puntos que se pueden explorar en profundidad

A través de los pasos anteriores, podemos implementar el editor de código bajo más simple, pero todavía hay muchos puntos técnicos que se pueden explorar en profundidad, enumeraré algunos de ellos y las soluciones a continuación.

8-1 ¿Cómo implementar el anidamiento de componentes?

Para el anidamiento de componentes, necesitamos modificar la lógica de operación para los datos del editor (es decir, datos jsonde formato ), desde la "inserción de matriz" original a una operación para un cierto nivel. Al mismo tiempo, la lógica de recorrido de componentes también debe ser cambió.

8-2 ¿Se puede abstraer un componente de edición de atributos de nivel superior?

La mayoría de los editores de la industria en realidad crean una capa de abstracción de este tipo.Para diferentes componentes de edición de atributos, se utilizarán para schemadescribir los elementos editables de este componente de edición y los datos que se pueden cambiar correspondientes a este elemento de edición. El componente de edición de atributos en realidad se usa para consumir esto schemapara la representación.

8-3 ¿Cómo subir un archivo relativamente grande como un video al servidor?

Esto implica cargar archivos en partes y almacenamiento combinado de back-end. Puede ver este proyecto que implementé antes: https://github.com/shadowings-zy/easy-file-uploader

8-4 ¿Una mejor solución de gestión de datos globales?

Debido a que la implementación es relativamente simple, este artículo coloca todos los datos bajo el componente raíz y luego los pasa como propsparámetros , pero de hecho, un componente de editor de código bajo complejo tiene una jerarquía profunda y propsno es realista usarlo. En este caso, sería más conveniente usar globales reduxcomo una biblioteca para la gestión de datos.

Supongo que te gusta

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