写了一个小儿科版的低代码Demo,低代码这个知识点就算踏足过了

1.效果演示

先插句题外话,本来是想插入一个视频的,去西瓜视频发布的时候,强制升级到抖音创作者中心平台,在抖音创作者中心发布录制视频后,无法像西瓜视频一样获取发布视频地址,好不容易在抖音app上找到发布视频地址,插入的时候却提示格式不对。只好曲线救国,把视频录制成gif动图,插入到文章中。但与视频相比,有些卡顿,想看视频的点击这里

现在我们进入正题,整个页面分为三部分:

  • 左侧侧边栏展示组件库,目前只有一个Button组件;
  • 中间是拖拽区域,分为编辑和预览两个面板,编辑面板组件的位置可以重新拖拽调整,可以删除组件,但是不响应属性面板配置的点击事件,预览面板则相反,会响应给组件绑定的事件,但不能拖拽移动,不能删除组件,不能配置组件的属性;
  • 右侧是选中组件属性的配置区域,可以配置选中组件的属性,比如说选中的是按钮组件,那么可以配置按钮组件的文案,背景色,点击事件,点击了应用按钮之后才会生效到画布中

看完视频,是不是感觉有点低代码平台雏形的样子,接下来我们看看如何实现演示的功能。

2.实现步骤

项目采用的技术栈是Vue3+Vite+Pinia+Less, 项目目录如下所示:

  • components文件夹下,放置了四个组件,分别是:
组件 说明
ComponentLibrary.vue 组件库列表
ConfigPanel.vue 组件属性配置面板
DraggableCanvas.vue 拖拽工作区
LowCodeButton.vue 自定义的Button组件
  • engines 组件注册与管理
  • stores 存储公共状态数据
  • App.vue 页面主文件
  • main.js 项目入口文件

2.1 先绘制页面布局

  • 左侧放置的是组件库<ComponentLibrary />, 展示组合页面的积木组件
  • 主内容区可以在编辑和预览面板之间进行切换,在编辑模式下,主内容区的拖拽工作区<DraggableCanvas />的内容可以编辑,在预览模式下是只读的;
  • 右侧是组件属性配置面板,展示选择组件的配置信息
html 复制代码
<!-- App.vue -->
<template>
  <div class="app">
    <div class="sidebar">
      <ComponentLibrary />
    </div>
    <div class="main-content">
      <div class="tabs">
        <button
          v-for="tab in tabs"
          :key="tab.key"
          :class="{ active: workspaceStore.mode === tab.key }"
          @click="() => handleModeChange(tab.key)"
        >
          {{ tab.label }}
        </button>
      </div>
      <DraggableCanvas ref="dragCanvasRef" :mode="workspaceStore.mode" />
    </div>
    <div class="config-panel">
      <ConfigPanel
        :selectedConfig="workspaceStore.configData"
        @update:config="updateComponentConfig"
      />
    </div>
  </div>
</template>
<script setup>
import { ref } from "vue";
import ComponentLibrary from "@/components/ComponentLibrary.vue";
import DraggableCanvas from "@/components/DraggableCanvas.vue";
import ConfigPanel from "@/components/ConfigPanel.vue";

import { useWorkspaceStore } from "@/stores/workspace";

const workspaceStore = useWorkspaceStore();

// 方法:更新组件配置
const dragCanvasRef = ref(null);
const updateComponentConfig = (newConfig) => {
  dragCanvasRef.value.updateComponentConfig(newConfig);
};

// Tabs 配置
const tabs = [
  { key: "edit", label: "编辑" },
  { key: "preview", label: "预览" },
];

const handleModeChange = (mode) => {
  workspaceStore.setMode(mode);
};
</script>

主工作区的公共数据流逻辑是:当切换编辑和预览模式时,调用workspaceStore.setMode(mode)变更模式状态,拖拽工作区和右侧组件属性配置面板接收到新的模式后,需要对自身绑定的事件响应行为进行禁止或启用。

主工作区接收到<ConfigPanel />组件上传的选中组件最新配置数据后,要调用<DraggableCanvas />组件中的updateComponentConfig(newConfig)方法,更新拖拽工作区组件的属性。

2.2 组件库的实现

  • step1: 先开发一个按钮组件,创建src\components\LowCodeButton.vue文件,内容如下:
html 复制代码
<template>
  <button
    :style="{ backgroundColor: config.color }"
    @click.stop="handleClick"
    class="low-code-button"
  >
    {{ config.label }}
  </button>
</template>

<script setup>
// import { watchEffect } from "vue";
import { useWorkspaceStore } from "@/stores/workspace";

// 定义 Props
const props = defineProps({
  config: {
    type: Object,
    default: () => ({
      label: "默认按钮",
      color: "#007bff",
      onClick: "",
    }),
  },
  ignoreClick: {
    type: Boolean,
    default: false,
  },
});

// watchEffect(() => {
//   console.log("props.config", props.config.label);
// });

// 处理点击事件
const workspaceStore = useWorkspaceStore();
const handleClick = () => {
  console.log("点击按钮");
  // 组件列表中的事件一律不触发
  if (props.ignoreClick || workspaceStore.mode === "edit") return;
  let onClickFn = props.config.onClick;

  try {
    if (props.config.onClick) {
      onClickFn = new Function(`return ${onClickFn}`)();
    }
  } catch (e) {
    console.error("解析点击事件代码失败:", e);
  }

  onClickFn && onClickFn();
};
</script>

组件接收两个参数configignoreClick, config是父组件传入进来的组件属性数据,ignoreClick用于控制是否执行按钮点击事件。在左侧组件库列表栏,是不允许执行按钮点击事件的。 还有工作区处于编辑状态时,也不响应点击事件。 由于点击事件回调函数是通过属性面板的input输入框配置的,所以是字符串格式,无法直接执行,需要创建一个立即表达式 ,用new Function('代码内容')()包裹一下,就像下面这样,才能正常运行。

js 复制代码
onClickFn = new Function(`return ${onClickFn}`)();
  • step2 注册组件,创建D:\low-code-platform\src\engines\index.js文件,内容如下: registerComponent用于注册组件,getRegisteredComponents用于获取组件列表, 定义完这两个函数后, 注册一个组件LowCodeButton
js 复制代码
// 示例组件
import LowCodeButton from "@/components/LowCodeButton.vue";

export const registeredComponents = {};

export function registerComponent(name, component, config) {
  registeredComponents[name] = { component, config };
}

export function getRegisteredComponents() {
  return registeredComponents;
}

// 注册默认组件
registerComponent("Button", LowCodeButton, {
  label: "默认按钮",
  color: "#007bff",
  onClick: () => {
    alert("点击按钮");
  },
});
  • step3 在主页展示注册的组件列表, 新建src\components\ComponentLibrary.vue文件,内容如下: 获取注册的组件列表,并进行遍历展示,为了禁止组件列表中的点击事件,给每个组件添加ignoreClick属性, 给组件列表容器添加 draggable="true"属性和dragstart事件,使其里面的组件可以拖拽。在dragstart事件回调中,将被拖拽组件的组件名和宽高传入目标容器。传递宽高是用于组件被放置到目标区域后,进行拖拽移动后,用于禁止拖动的可移动区域的判断。
html 复制代码
<template>
  <div class="component-library">
    <h3>组件库</h3>
    <div
      v-for="(item, name) in components"
      :key="name"
      class="component-item"
      draggable="true"
      @dragstart="onDragStart($event, name)"
    >
      <component :is="item.component" ignoreClick />
    </div>
  </div>
</template>

<script setup>
import { getRegisteredComponents } from "@/engines/index";

const components = getRegisteredComponents();

const onDragStart = (event, componentName) => {
  console.log("onDragStart", event);
  // 获取元素的宽度和高度
  const rect = event.target.getBoundingClientRect();
  const width = rect.width;
  const height = rect.height;
  // 将尺寸信息作为自定义数据传递
  event.dataTransfer.setData(
    "componentInfo",
    JSON.stringify({ componentName, width, height })
  );
};
</script>

2.3 主工作区的实现

编辑和预览两个tab的切换功能比较简单,就不展开说了。重点说一下拖拽区域的功能实现。创建画布文件src\components\DraggableCanvas.vue,内容如下:

html 复制代码
<template>
  <div class="play-ground" @dragover.prevent @drop.stop="onDrop">
    <div
      v-for="(item, index) in compList"
      :key="index"
      :style="{
        top: item.position.top + 'px',
        left: item.position.left + 'px',
      }"
      :class="[
        'draggable-item',
        { selected: selectedComponentIndex === index },
      ]"
      @mousedown="onMouseDown(index, $event)"
      @click.capture="selectComponent(index)"
    >
      <!-- 组件渲染 -->
      <component :is="item.component" :config="item.config" />

      <!-- 删除按钮 -->
      <button
        v-if="selectedComponentIndex === index && props.mode === 'edit'"
        class="delete-btn"
        @click.stop="deleteComponent(index)"
      >
        x
      </button>
    </div>
  </div>
</template>

<script setup>
import { reactive, ref, watchEffect } from "vue";
import { registeredComponents } from "@/engines/index";
import { useWorkspaceStore } from "@/stores/workspace";
import { cloneDeep } from "lodash-es";

const props = defineProps({
  mode: Boolean,
});

// 工作区画布组件列表
const compList = reactive([]);
// 选中组件索引号
const selectedComponentIndex = ref(-1);

// watchEffect(() => {
//   console.log(compList);
// });

// 选中组件时, 设置组件属性面板配置数据
const workspaceStore = useWorkspaceStore();
// 组件的宽高
let selectComponentRect = {
  width: 0,
  height: 0,
};

// 处理拖放事件
const onDrop = (event) => {
  console.log("onDrop");
  if (workspaceStore.mode === "preview") return;
  const componentInfo = event.dataTransfer.getData("componentInfo");
  const { componentName, width, height } = JSON.parse(componentInfo);
  selectComponentRect = { width, height };
  if (!componentName) return;

  const { component, config } = registeredComponents[componentName];

  const newComponent = {
    component,
    config, // 配置对象
    position: {
      top: event.offsetY,
      left: event.offsetX,
    },
  };

  compList.push(newComponent);
  selectedComponentIndex.value = compList.length - 1;
  selectComponent(selectedComponentIndex.value);
};

// 鼠标拖动逻辑
const onMouseDown = (index, event) => {
  console.log("onMouseDown");
  if (workspaceStore.mode === "preview") return;

  const selectedComponent = compList[index];

  let initX = event.clientX;
  let initY = event.clientY;

  const onMouseMove = (moveEvent) => {
    // 就算x,y坐标偏移量
    const dx = moveEvent.clientX - initX;
    const dy = moveEvent.clientY - initY;
    // 计算新的初始位置
    initX = moveEvent.clientX;
    initY = moveEvent.clientY;

    // 计算新的位置
    let newTop = selectedComponent.position.top + dy;
    let newLeft = selectedComponent.position.left + dx;

    // 限制在画布范围内
    const playGround = document.querySelector(".play-ground");
    const { width, height } = playGround.getBoundingClientRect();

    newTop = Math.max(0, Math.min(newTop, height - selectComponentRect.height));
    newLeft = Math.max(0, Math.min(newLeft, width - selectComponentRect.width));

    selectedComponent.position.top = newTop;
    selectedComponent.position.left = newLeft;
  };

  const onMouseUp = (event) => {
    console.log("onMouseUp");

    window.removeEventListener("mousemove", onMouseMove);
    window.removeEventListener("mouseup", onMouseUp);
  };

  window.addEventListener("mousemove", onMouseMove);
  window.addEventListener("mouseup", onMouseUp);
};

const selectComponent = (index) => {
  if (workspaceStore.mode === "preview") return;

  selectedComponentIndex.value = index;
  workspaceStore.setData(cloneDeep(compList[index].config));
};

// 暴露给父组件的方法-更新组件属性
const updateComponentConfig = (config) => {
  compList[selectedComponentIndex.value].config = cloneDeep(config);
};

defineExpose({
  updateComponentConfig,
});

// 删除组件
const deleteComponent = (index) => {
  compList.splice(index, 1);
  selectedComponentIndex.value = null;
  workspaceStore.setData({});
};
</script>

功能主要有三点:

  1. 组件拖拽之后的放置逻辑
  2. 组件的移动逻辑
  3. 组件的选中和删除逻辑

在讲这四个功能之前,先看看用到的公共状态数据。左侧组件列表,主内容区,右侧属性配置面板公用的数据是选中组件的属性configData和工作区的模式mode,当模式为预览时,需要清空右侧的属性配置面板数据。

js 复制代码
import { defineStore } from "pinia";

export const useWorkspaceStore = defineStore("workspace", {
  state: () => ({
    configData: {},
    mode: "edit",
  }),
  getters: {},
  actions: {
    setData(data) {
      this.configData = data;
    },
    setMode(mode) {
      this.mode = mode;
      if (mode === "preview") {
        this.configData = {};
      }
    },
  },
});
2.3.1 组件拖拽之后的放置逻辑

拖拽目标元素只有设置了@dragover.prevent属性,drop事件才能被监听到

html 复制代码
<div class="play-ground" @dragover.prevent @drop.stop="onDrop">

当组件库列表的组件被放置到工作区后,会触发在onDrop函数的执行,在onDrop函数里,先判断是不是预览模式,如果不是,获取拖拽源事件传递过来的组件参数(组件名,组件的宽高),然后从注册组件列表中取出对应的组件,并追加从drop事件中获取组件移动位置,添加到工作区的渲染列表compList中,就实现的拖拽组件的功能。此外,还要标记一下当前添加元素为选中元素,在右侧属性配置面板展示添加元素的配置信息。

2.3.2 组件的移动逻辑

当在画布中移动组件时,会触发onMouseDown函数执行,在onMouseDown函数中,先将初始移动位置保存出来,然后全局注册mousemovemouseup事件,分别用于监听移动事件和移动结束事件。在移动事件回调中,计算位置移动增量,判断被移动的组件,是否触达拖拽工作区的边界。横向和纵向的最大能移动距离分别是width - selectComponentRect.widthheight - selectComponentRect.height

js 复制代码
    // 计算新的移动位置
    let newTop = selectedComponent.position.top + dy;
    let newLeft = selectedComponent.position.left + dx;

    // 限制在画布范围内
    const playGround = document.querySelector(".play-ground");
    const { width, height } = playGround.getBoundingClientRect();

    newTop = Math.max(0, Math.min(newTop, height - selectComponentRect.height));
    newLeft = Math.max(0, Math.min(newLeft, width - selectComponentRect.width));

    selectedComponent.position.top = newTop;
    selectedComponent.position.left = newLeft;

当组件移动结束后,会触发mouseup事件,在mouseup事件回调中,移除对全局mousemovemouseup事件的监听。

2.3.3 组件的选中和删除逻辑

画布中的组件被选中后,要将选中组件的配置属性数据存储到全局store中,右侧属性配置面板会接收到更新选中组件的配置数据。组件被删除时,会清空属性面板数据。

2.4 配置面板的实现

配置面板用于选中组件的属性编辑,创建src\components\ConfigPanel.vue文件,内容如下: 配置面板接收到主界面传递的选中组件属性数据后,会按key-value的方式进行遍历渲染,key采用label标签展示,value采用input标签展示。点击了应用按钮之后,才向主页面传递最新的组件属性值, 触发拖拽工作区的选中组件属性更新。

html 复制代码
<template>
  <div class="config-panel">
    <h3>属性面板</h3>
    <div v-if="Object.keys(selectedConfig).length">
      <div v-for="(value, key) in selectedConfig" :key="key" class="form-group">
        <label>{{ key }}</label>
        <input v-model="selectedConfig[key]" />
      </div>
      <button @click="applyChanges">应用配置</button>
    </div>
    <div v-else>
      <p>未选中任何组件</p>
    </div>
  </div>
</template>

<script setup>
const props = defineProps({
  selectedConfig: Object, // 当前选中组件的配置信息
});

// 定义组件属性修改事件
const emit = defineEmits(["update:config"]);

// 组件属性修改事件通知
const applyChanges = () => {
  emit("update:config", props.selectedConfig);
};
</script>

最后

至此,这个简易版的低代码demo功能就实现了。这个低代码平台demo纯属自娱自乐。与市面上的商用低代码平台没有可比性。唯一的意义就在于化繁为简,理解商用低代码平台最基础的功能的实现方式。好了,今天的分享就到这里,各位掘友,下期见。

相关推荐
哑巴语天雨5 分钟前
React+Vite项目框架
前端·react.js·前端框架
初遇你时动了情18 分钟前
react 项目打包二级目 使用BrowserRouter 解决页面刷新404 找不到路由
前端·javascript·react.js
乔峰不是张无忌33037 分钟前
【HTML】动态闪烁圣诞树+雪花+音效
前端·javascript·html·圣诞树
鸿蒙自习室1 小时前
鸿蒙UI开发——组件滤镜效果
开发语言·前端·javascript
m0_748250741 小时前
高性能Web网关:OpenResty 基础讲解
前端·openresty
前端没钱1 小时前
从 Vue 迈向 React:平滑过渡与关键注意点全解析
前端·vue.js·react.js
NoneCoder1 小时前
CSS系列(29)-- Scroll Snap详解
前端·css
无言非影1 小时前
vtie项目中使用到了TailwindCSS,如何打包成一个单独的CSS文件(优化、压缩)
前端·css
我曾经是个程序员2 小时前
鸿蒙学习记录
开发语言·前端·javascript
羊小猪~~2 小时前
前端入门之VUE--ajax、vuex、router,最后的前端总结
前端·javascript·css·vue.js·vscode·ajax·html5