学习低代码编辑器第三天

本文将接着上回继续编写低代码编辑器框架(代码含详细注释)

前言

在上文中,我们已经完成了:

  1. useComponentsStore仓库(用于存放整个json树以及其对应的方法)
  2. Page组件、Button组件、Container组件
  3. useComponentConfigStore仓库(用于存放所有以及创建好的组件实例,以及注册组件的方法)
  4. renderComponents渲染函数(将json树中所有的组件对象渲染到画布区)
  5. 将已经打造好的组件名称展示在了物料区

本文将接着上文实现从左边物料区拖拽组件,并放置在中间的物料区进行展示

创建MaterailItem/index.tsx子组件

在Materail/index.tsx中,之前是通过map方法将useComponentConfigStore仓库被组件实例的名称获取并创建一个div容器进行包裹,那么现在为了更好的实现拖拽效果,我们将div包裹的内容封装成一个子组件,在Materail/index.tsx中调用该子组件即可

1. 在src/editor/components文件夹下创建MaterailItem文件夹,并在其下创建index.tsx文件

2. 将Materail/index.tsx中的div剪切到MaterailItem/index.tsx子组件中

3. 将子组件引入Materail/index.tsx中

js 复制代码
//Materail/index.tsx完整代码
import { useComponentConfigStore } from '../../stores/component-config'
import { useMemo } from 'react'//用于缓存
import MaterialItem from '../MaterialItem'
export default function Materail() {
  const { componentConfig } = useComponentConfigStore()//取出仓库内的组件配置
  const components = useMemo(() => {
    return Object.values(componentConfig)//得到一个包含已创建的组件配置实例的数组
  }, [componentConfig])
  //只有当依赖数组中的 componentConfig 发生变化时,才会重新执行箭头函数并计算新的 components 数组。
  // 如果 componentConfig 没有变化,useMemo 会直接返回上一次缓存的结果,避免重复执行 Object.values().filter() 操作。
  return (
    <div>
      {
        components.map((item, index) => {
          return (
          //key属性为React框架自用属性,不参与propos传递,只传递name属性
            <MaterialItem key={item.name + index} name={item.name} />
          )
        })
      }
    </div>
  )
}

4. 实现物料区组件的拖拽效果

  • 引入之前安装的React DnD库中的 useDrag 钩子实现了组件的拖拽功能
js 复制代码
import { useDrag } from "react-dnd"//提供拖拽功能
  • 使用useDrag钩子函数生成拖拽引用dragRef
js 复制代码
const [_, dragRef] = useDrag(() => ({//dragRef表示拖拽引用,绑定到DOM元素上使其成为拖拽目标
//表示拖拽类型表示符,确保不同物料项有不同的拖拽类型,作用是:可将其引入父容器中,明确父容器可盛放该组件
        type: props.name,
        item: { //被拖动的类型信息,父容器可通过monitor.getItem()获取此对象,用于处理拖拽逻辑
            type: props.name
        }
    })
)
  • 绑定到div容器上,使其成为拖拽目标
js 复制代码
//MaterailItem/index.tsx完整代码
import { useDrag } from "react-dnd"//提供拖拽功能
export interface MaterialItemProps {//自己定义的props的参数类型
    name: string
}
export default function MaterialItem(props: MaterialItemProps) {
//props为父组件传给子组件的参数,类型MaterialItemProps为我们自己定义的接口
//_表示忽略第一个返回值
    const [_, dragRef] = useDrag(() => ({//dragRef表示拖拽引用,绑定到DOM元素上使其成为拖拽目标
    //type: 是拖拽源与放置目标之间的 "匹配标识"------ 放置目标通过 `accept` 数组声明可接收的 `type`,只有两者匹配时,放置目标才会响应拖拽行为
        type: props.name,
        item: { //被拖动的类型信息,父容器可通过monitor.getItem()获取此对象,用于处理拖拽逻辑
            type: props.name
        }
    })
    )
    return (
        <div
            ref={dragRef as any}//将dragRef绑定在包裹每个组件实例名称的div上使其成为拖拽目标
            className=' 
            border-dashed 
            border-[1px] 
            border-[#000] 
            py-[8px] px-[10px] 
            inline-block 
            bg-white
            m-[10px]
            cursor-pointer
             hover:bg-[#ccc]
            '
            key={props.name}>
            {props.name}//展示的组件名称
        </div>
    )
}

至此,拖拽效果实现,接下来进行Page和Container容器组件的配置,使其能够盛放子组件,从而达到将物料区组件拖拽并放置在画布区内的效果

在此之前,需做一些准备

  • 先修改一点小样式,使得json树最外层定死的Page组件的高度和页面一样高,修改src/editor/components/EditArea/index.tsx文件中的div容器的样式,将个h-[100%]
js 复制代码
return (
    <div className=' h-[100%]'>
      {renderComponents(components )}
    </div>
  )
  • 在editor文件夹下创建一个公共接口interface.ts,用于定义一个公共类型(作为基础组件属性接口,统一规范不同组件的公共属性)
js 复制代码
import type {PropsWithChildren} from 'react'
////PropsWithChildren为React内置泛型接口,提供 children?: ReactNode 属性,允许组件接收子元素
//CommonComponentProps通过 extends PropsWithChildren 使接口拥有接收子元素的能力
export interface CommonComponentProps extends PropsWithChildren {
    id: number
    name: string
    [key:string]:any
    //[key:string] :支持任意字符串类型的属性名
    //any :属性值可以是任意类型
    //作用:提供灵活性,允许组件接收未预先定义的动态属性
}
  • 在useComponentConfigStore仓库中的组件对象中再添加一个desc属性
    component-config.tsx文件内,要修改的代码如下
js 复制代码
export interface ComponentConfig {
    name: string;//组件名称
    defaultProps: Record<string, any>;//组件默认属性
    component: any// 实际的 React 组件
    desc:string//组件描述
}

componentConfig: {
        Container: {
            name: "Container",
            defaultProps: {},
            component: Container,
            desc:'容器',
        },
        Button: {
            name: "Button",
            defaultProps: {
                type: 'primary',//按钮类型
                text: '按钮'//按钮名称
            },
            component: Button,
            desc:'按钮',
        },
        Page: {
            name: "Page",
            defaultProps: {},
            component: Page,
            desc:'页面'
        },
    },

自创建一个钩子函数useMaterialDrop,可被Page和container容器使用,使他们能够放置拖拽过来的组件,并能够限定可放置组件的类型

1. 在editor文件夹下创建hooks文件夹,hook文件夹内再创建useMaterialDrop.ts文件

2. 完成钩子函数useMaterialDrop的实现逻辑

大体思路 :useMaterialDrop函数需接收两个参数,一个是accept: string[] ,用于限制放置组件的类型,另一个是使用该钩子函数的容器id ,用于在添加拖拽项到json树中时设置的父组件id。useMaterialDrop函数中主要使用了React DnD库内的放置目标钩子函数useDrop(),实现了:

  1. 限制放置组件的类型accept
  2. 使用Ant Design消息提示组件弹出添加成功信息
  3. 调用useComponentsStore仓库中的addComponent方法将拖拽项添加至json树中
  4. 收集拖拽状态canDrop(供组件样式使用)

最后函数返回三个参数
canDrop :是否允许放置(用于条件渲染样式)
dropRef : 放置区域DOM引用(需绑定到容器元素,使其可放置组件)
contextHolder:Ant Design消息组件的上下文(需在组件中渲染)

js 复制代码
useMaterialDrop.ts完整代码
import { useDrop } from 'react-dnd'// React DnD库的放置目标钩子
import { useComponentsStore } from '../stores/components'  // 组件状态管理仓库
import { useComponentConfigStore } from '../stores/component-config'// 组件配置管理仓库
import { message } from 'antd'// Ant Design消息提示组件
//accept: string[] :允许接收的拖拽类型数组(与拖拽源的 type 匹配),就是刚刚设置的type: props.name
//id: number :当前容器组件的 ID,用于给新添加的组件设置父组件 ID
export function useMaterialDrop(accept: string[], id: number) {
    const { addComponent } = useComponentsStore()//从useComponentsStore获取addComponent方法
    const { componentConfig } = useComponentConfigStore()//从useComponentConfigStore获取componentConfig对象
    const [messageApi, contextHolder] = message.useMessage();
    //useDrop是React DnD的核心钩子,用于定义放置目标行为,返回: canDrop: 是否允许放置(由collect函数计算)
    const [{ canDrop }, dropRef] = useDrop(() => {
        return {
            accept, // 允许接收的拖拽类型(与参数accept一致,所以就简写成一个)
            // 拖拽完成时触发的回调函数
            drop: (item: { type: string }, monitor) => {//item为从拖拽源传过来的对象,里面放的就是拖拽源组件的名称
                // 检查是否已在其他目标放置(避免重复处理)
                //monitor.didDrop()默认结果为false,如果在page容器内添加了一个container组件,那么monitor.didDrop()就会变为true,
                // 再想在container容器内放一个button,这时候就不会误将button也加入到page中,而是只会加在container容器内
                const didDrop = monitor.didDrop()
                if (didDrop) return//如果didDrop为true,说明已经在其他目标放置了,就直接返回
                // 显示成功消息
                messageApi.success(`${item.type}添加成功`)
                // 从组件配置仓库中获取默认属性和描述,item.type为对应的组件名称
                const props = componentConfig?.[item.type]?.defaultProps
                const desc = componentConfig?.[item.type]?.desc
                // 调用useComponentsStore仓库内的添加组件对象到json树的方法,将拖拽组件添加到json树中,然后会触发渲染函数的执行
                addComponent({
                    id: new Date().getTime(), // 使用时间戳作为唯一ID
                    name: item.type,          // 组件类型名称
                    props: props,             // 默认属性
                    desc: desc                // 组件描述
                }, id) // id为父容器ID,指定组件添加位置
            },
            // 收集拖拽状态(供组件使用)
            collect: (monitor) => ({
                // 当前是否可以放置拖拽项,会被利用到容器样式内,如果是可以放置的拖拽项,则组件边框变为设置好的颜色
                canDrop: monitor.canDrop()
            })
        }
    })
    return {
        canDrop,// 是否允许放置(用于条件渲染样式)
        dropRef,// 放置区域DOM引用(需绑定到容器元素,使其可放置组件)
        contextHolder// Ant Design消息组件的上下文(需在组件中渲染)
    }
}

3.将钩子函数引入到contain组件中

关键步骤:解构父组件传过来的id和name属性,并将参数类型改为CommonComponentProps公共接口类型

调用useMaterialDrop函数,传入需要限定的拖拽项的类型,以及组件的id,分别拿到canDrop, dropRef, contextHolder三个参数

将contextHolder加入到DOM元素中实现添加成功消息提示

将dropRef绑定DOM元素,使其能够放置子组件

将canDrop添加到样式中,定义可放置项和不可放置项的不同样式

js 复制代码
//Container/index.tsx完整代码
import type { CommonComponentProps } from '../../interface'//引入公共接口类型
import { useMaterialDrop } from '../../hooks/useMaterialDrop'//引入自建钩子函数,用于处理拖拽放置逻辑
//id为渲染组件时添加的id属性,表示当前组件的id,此id就是父传子参数中获取的
//children使得该组件在父组件内也可以在该组件内放子结构体(相当于一个容器组件)
export default function Container({ id, name, children }: CommonComponentProps) {
  //['Container', 'Button']表示该容器组件可以放入的组件类型
  //id参数用于钩子函数内添加拖拽项时设置该容器为父组件id
  const { canDrop, dropRef, contextHolder } = useMaterialDrop(['Container', 'Button'], id)
  return (
    <>
      {/* Ant Design消息组件的上下文,用于在容器组件内显示消息 */}
      {contextHolder}
      <div
        ref={dropRef as any}//将DOM元素注册为React DnD的放置目标,可放置拖拽项
        className={`min-h-[100px] p-[20px]
      ${canDrop ? 'border-[2px] border-[blue]' : 'border-[1px] border-[#000] '}`}>
        {/* canDrop === true:拖拽项可放置时,显示2px蓝色边框(视觉反馈)  canDrop === false:默认显示1px黑色边框 */}
        {children}
      </div>
    </>
  )
}

4.将钩子函数引入到Page组件中(注释同上)

js 复制代码
//Page/index.tsx完整代码
import type { CommonComponentProps } from '../../interface'
import { useMaterialDrop } from '../../hooks/useMaterialDrop'
export default function Page({ id, name, children }: CommonComponentProps) {//使得该组件在父组件内也可以在该组件内放子结构体(相当于一个容器组件)
  const { canDrop, dropRef, contextHolder } = useMaterialDrop(['Container', 'Button'], id)
  return (
    <>
      {contextHolder}
      <div
        ref={dropRef as any}
        className=" p-[20px] h-[100%] box-border"
        style={{ border: canDrop ? '2px solid red' : 'none'}}
      >
        {children}
      </div>
    </>

  )
}

效果图

总结

今天实现了物料区组件的拖拽功能,并将其放置在中间画布区的容器内,且完成了展示

下期将实现鼠标移动到画布区的某个组件上时,该组件则显示一层浅紫色蒙层

相关推荐
月阳羊3 小时前
【硬件-笔试面试题】硬件/电子工程师,笔试面试题-26,(知识点:硬件电路的调试方法:信号追踪,替换,分段调试)
笔记·嵌入式硬件·面试·职场和发展
爷_3 小时前
字节跳动震撼开源Coze平台!手把手教你本地搭建AI智能体开发环境
前端·人工智能·后端
lemonth4 小时前
个人发展之路
面试
charlee444 小时前
行业思考:不是前端不行,是只会前端不行
前端·ai
Amodoro5 小时前
nuxt更改页面渲染的html,去除自定义属性、
前端·html·nuxt3·nuxt2·nuxtjs
Wcowin5 小时前
Mkdocs相关插件推荐(原创+合作)
前端·mkdocs
伍哥的传说6 小时前
CSS+JavaScript 禁用浏览器复制功能的几种方法
前端·javascript·css·vue.js·vue·css3·禁用浏览器复制
lichenyang4536 小时前
Axios封装以及添加拦截器
前端·javascript·react.js·typescript
Trust yourself2436 小时前
想把一个easyui的表格<th>改成下拉怎么做
前端·深度学习·easyui
三口吃掉你6 小时前
Web服务器(Tomcat、项目部署)
服务器·前端·tomcat