学习低代码编辑器第二天

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

前言

接下来主要实现物料区,画布区和属性区的逻辑,头部组件后续再接着完善,其中最核心的逻辑包括:

  1. 实现从物料区将组件拖拽到画布区并展示的效果,实质上就是维护一个json对象,每次拖拽添加组件,就是将其添加至json对象的某一层,再进行渲染
  2. 右侧的属性区,实质上就是添加,删除,修改,组件对象中的属性值和样式值
  3. 最后将json展示成树状图

进行components.tsx文件的编写

在编写前,先将之前写的mode状态变量和setMode方法先删除,后续会进行补充,现在开始先在仓库内创建json对象,以及在json对象中实现添加组件对象,删除组件对象,更新组件对象属性的方法

1. 引入并使用create方法创建useComponentsStore仓库

js 复制代码
import { create } from 'zustand'
export const useComponentsStore = create<State & Action>((set, get) => ({}))
//State表示create函数内状态变量的类型,Action表示方法的类型

2. 添加json对象数组components[]

  • 先限定create函数状态变量的类型为State(自己打造的类型)
    1. Component为components数组内对象的类型
    2. Component[]则为components数组的类型
js 复制代码
export interface Component {
    id: number,//组件id
    name: string,//组件名字
    props: any,//组件属性
    desc: string,
    children?: Component[],//组件的子组件(可有可无)
    parentId?: number,//组件父容器的id(可有可无)
}
export interface State {
    components: Component[]
}
  • 设置固定的json对象中的数据(也就是最外层的json对象),后续添加都是用户手动拖拽组件进行添加
js 复制代码
export const useComponentsStore = create<State & Action>((set, get) => ({
    components: [//整个项目的json数据
        {
            id: 1,
            name: 'Page',
            props: {},
            desc: '页面',
        }
    ],
    }))

3. 创建添加组件对象,删除组件对象,更新组件对象属性的方法

  • 先规限定定create函数中方法的类型为Action(自己打造的类型)
js 复制代码
export interface Action {
    addComponent: (component: any, parentId: number) => void
    //component表示要添加的组件,parentId表示父容器的id,知道子组件加到哪个父容器内,函数为void类型
    deleteComponent: (componentId: number) => void
    //componentId表示要删除的组件id,函数为void类型
    updateComponentProps: (componentId: number, props: any) => void
    //更新组件属性,props表示要更新的属性,函数为void类型
}
  • 先打造一个getComponentById函数,用于通过组件id来获取对应组件对象(可以用来找父容器或己组件)

大体思路为,遍历components数组,在最外层结构中找不到所要的组件对象,则使用递归向里层继续找

js 复制代码
export function getComponentById(id: number | null, components: Component[]): Component | null {//输入自己或父组件的id,找到自己或父组件
    //如果id无值,则直接返回null
    if (!id) return null 
    for (const component of components) {//遍历components数组
        if (component.id === id) {//如果最外层可以找到所要的组件,则直接返回该组件
            return component
        }
        //如果最外层找不到,则一步一步向内递归,从子组件中找
        if (component.children && component.children.length > 0) {
            //将最终找到的组件对象赋值给result
            const result = getComponentById(id, component.children)
            //如果result存在,则返回该组件对象
            if (result) {
                return result
            }
        }
    }
    return null//如果id在json对象中不存在,则直接返回null
}
  • 打造addComponent方法,用于添加组件对象至json对象中

大体思路:第一步考虑的是有父容器的情况,先找到父容器,在将组件对象添加至父容器的children属性中,第二就是没有父容器的情况,则直接添加在component数组的最外层结构

  • 打造deleteComponent方法,用于将json对象中对应的组件对象删除

大体思路:获取要删除的组件对象的父容器,然后使用filter方法将要删除的子组件过滤掉就行

  • 打造updateComponentProps方法,用于更新组件对象中的props属性

大体思路:拿到更新属性的组件,将传入的属性和已有属性进行合并就行
至此components.tsx仓库内基础的数据和方法创建完毕

js 复制代码
//stores/components.tsx完整代码
 import { create } from 'zustand'
 export interface Component {
    id: number,//组件id
    name: string,//组件名字
    props: any,//组件属性
    desc: string,
    children?: Component[],//组件的子组件(可有可无)
    parentId?: number,//组件父容器的id(可有可无)
}
export interface State {
    components: Component[]
}
export interface Action {
    addComponent: (component: Components, parentId: number) => void
    //component表示要添加的组件,parentId表示父容器的id,知道子组件加到哪个父容器内,函数为void类型
    deleteComponent: (componentId: number) => void
    //componentId表示要删除的组件id,函数为void类型
    updateComponentProps: (componentId: number, props: any) => void
    //更新组件属性,props表示要更新的属性,函数为void类型
}
export const useComponentsStore = create<State & Action>((set, get) => ({//set:触发视图更新 get:获取当前状态(不会触发组件渲染)
    components: [//整个项目的json数据
        {
            id: 1,
            name: 'Page',
            props: {},
            desc: '页面',
        }
    ],
    //方法
    addComponent: (component, parentId) => {//本质上就是将一个对象添加到另一个对象中(json对象)
        set((state) => {
            if (parentId) {//如果父容器id存在
                //获取父级对象
                //引用getComponentById函数,将父组件id和state.components(实际上就是刚刚定义的components[]数组)传入函数中,获取到父组件对象parentComponent
                const parentComponent = getComponentById(parentId, state.components)

                //如果父组件对象存在,执行以下三元运算符
                if (parentComponent) {
                    parentComponent.children ? parentComponent.children.push(component) : parentComponent.children = [component]//(因为children属性的值为数组)
                    //如果父组件有children属性,直接push,没有则创建,并将component放到数组内后赋值
                }
                component.parentId = parentId//设置添加的组件对象的父组件id
                return {
                    components: [...state.components]
                }
            }
            return {
                components: [...state.components, component]
                //如果没有父级,则直接添加到仓库最外层:
                // 1. 使用展开运算符将原state.components数组展开
                // 2. 将新组件component追加到末尾
                // 3. 组合成一个全新的数组
                // 4. 赋值给components属性

            }
        })
    },
    deleteComponent: (componentId) => {//在整个json对象中找到某个子对象的id,然后删除
        if (!componentId) return
        //拿到要删除的子组件
        //调用getComponentById方法就能拿到对应的组件对象(其实get().components就是state.components,目的都是拿到仓库中的components[])
        const component = getComponentById(componentId, get().components)
        //如果子组件有父组件的id
        if (component?.parentId) {
            //拿到要删除的子组件的父组件
            const parentComponent = getComponentById(component.parentId, get().components)
            if (parentComponent) {
                //使用filter方法对父组件的children数组进行过滤,将id不等于要删除的子组件id的子组件对象组成一个新数组返回
                parentComponent.children = parentComponent.children?.filter((item) => item.id !== componentId)
            }
            //引用set方法触发试图更新和重新渲染,将过滤之后的数组赋值给components[]
            set({
                components: [...get().components]
            })
        }
    },
    //更新组件属性
    updateComponentProps: (componentId, props) => {//传入组件id和要添加的属性
        set((state) => {
            //获取要操作的组件
            const component = getComponentById(componentId, state.components)
            if (component) {//如果找到了要操作的组件对象
                component.props = { ...component.props, ...props }//将旧属性和新属性合并
                return {
                    //将更新后的conponents解构赋值为新的components,也就创建了一个新的引用,以达到视图更新的目的s
                    components: [...state.components]
                }
            }
            //如果没有找到组件对象,则也创建了一个新的引用,将原来的components[]解构赋值给新的components,以达到视图更新的目的
            return { components: [...state.components] }
        })
    },
}))

export function getComponentById(id: number | null, components: Component[]): Component | null {//输入自己或父组件的id,找到自己或父组件
    //如果id无值,则直接返回null
    if (!id) return null 
    for (const component of components) {//遍历components数组
        if (component.id === id) {//如果最外层可以找到所要的组件,则直接返回该组件
            return component
        }
        //如果最外层找不到,则一步一步向内递归,从子组件中找
        if (component.children && component.children.length > 0) {
            //将最终找到的组件对象赋值给result
            const result = getComponentById(id, component.children)
            //如果result存在,则返回该组件对象
            if (result) {
                return result
            }
        }
    }
    return null//如果id在json对象中不存在,则直接返回null
}

分别创建Page组件,Container组件和Button组件

先在editor文件夹下创建materials文件夹,用于盛放组件

在materials文件夹下分别创建Page、Container、Button文件夹,再分别在这三个文件夹下创建属于自己的index.tsx文件

1. 创建Page组件

js 复制代码
import type { PropsWithChildren } from "react";
export default function Page({ children }:PropsWithChildren ) {
//{ children }参数使得该组件在使用时,内部也可以放子组件,并进行渲染(相当于一个容器组件)
  return (
    <div className=" p-[20px] h-[100%] box-border"> 
      {children}
    </div>
  )
}

2. 创建Container组件

js 复制代码
import type { PropsWithChildren } from "react";
//{ children }参数使得该组件在使用时,内部也可以放子组件,并进行渲染(相当于一个容器组件)
export default function Container({ children }:PropsWithChildren ) {
  return (
    <div className=" border-[1px] border-[#000] min-h-[100px] p-[20px]"> 
      {children}
    </div>
  )
}

3. 创建Button组件

js 复制代码
import { Button as AntdButton } from "antd"//引入antd库内的Button组件
import type {ButtonType} from 'antd/es/button'//引入antd自带的ButtonType类型
interface ButtonProps {//自定义类型,限制type,text属性的类型
  type: ButtonType
  text: string
}
export default function Button({type,text}:ButtonProps) {
  //text和type父组件向子组件传的值,type表示按钮的类型(属于antd按钮的类型),text表示按钮的文本
  return (
    <AntdButton type={type}>{text}</AntdButton>
  )
}

接下来需要创建一个仓库,使得可以通过组件名来拿到对应的组件(也叫组件的映射)

创建useComponentConfigStore仓库

1. 在stores文件夹下创建component-config.tsx文件

2. 引入刚刚创建的组件

js 复制代码
import Container from "../materials/Container/index";
import Button from "../materials/Button/index";
import Page from "../materials/Page/index";

3. 引入并使用create方法创建useComponentConfigStore仓库

js 复制代码
import { create } from 'zustand'
export const  useComponentConfigStore= create<State & Action>((set, get) => ({}))
//State表示create函数内状态变量的类型,Action表示方法的类型

4. 添加状态变量componentConfig对象(用于盛放各个已创建的组件)

  • 需先创建限定componentConfig对象的类型,以及对象内元素的类型
  • 需先限定create函数状态变量的类型为State (自己打造的类型)
    1. ComponentConfig为componentConfig对象内值的类型
    2. State为componentConfig对象的类型
js 复制代码
export interface ComponentConfig{
    name:string;//组件名称
    defaultProps:Record<string,any>;//组件默认属性
    component:any//  组件实例
}
export interface State {
    componentConfig: {
        //表示componentConfig对象的类型,任何字符串类型的值都能做key,而值的类型限定为刚刚创建的ComponentConfig类型
        [key: string]: ComponentConfig
    }
}
  • 将刚刚创建的组件实例添加进componentConfig对象内
js 复制代码
export const  useComponentConfigStore= create<State & Action>((set, get) => ({
    componentConfig:{
        Container:{
            name:"Container",
            defaultProps:{},
            component:Container,
        },
        Button:{
            name:"Button",
            defaultProps:{
                type:'primary',//按钮类型
                text:'按钮'//按钮名称
            },
            component:Button,
        },
        Page:{
            name:"Page",
            defaultProps:{},
            component:Page,
        },
    },
}))

5. 创建registerComponent方法(用于注册新组件)

  • 先规限定定create函数中方法的类型为Action(自己打造的类型)
js 复制代码
export interface Action{
    registerComponent:(name:string,componentConfig:ComponentConfig)=>void//注册组件
}
  • 添加registerComponent方法
js 复制代码
registerComponent:(name,componentConfig)=>{//注册组件,自定义组件
    //name表示注册的新组件的名字,componentConfig为新组件的属性参数
        set((state)=>{
            return{
                componentConfig:{
                    ...state.componentConfig,//保留原有的组件
                    [name]:componentConfig//添加新的组件
                }
            }
        })
    }

至此,前期的useComponentConfigStore仓库创建完毕

在EditArea/index.tsx文件中打造渲染函数(画布区)

主要思路:需要用到刚刚打造的useComponentsStore仓库,用于获取组件的名称,方便通过组件名称,从useComponentConfigStore仓库内拿到要渲染的组件实例,再使用React自带的createElement进行渲染,并可以在渲染后的DOM元素上添加属性,和递归渲染(因为json数据内部有children属性,一次渲染只能渲染最外层,所以要使用递归渲染)

1.先将所需要的仓库,类型,和模块引入

js 复制代码
import type { Component } from '../../stores/components'//将components仓库内的Component类型引入
import { useComponentConfigStore } from '../../stores/component-config'
import React from 'react'
import { useComponentsStore } from '../../stores/components'

2.再获取useComponentsStore仓库内的components[]

  • 用于遍历json数据,拿到最外层组件的名称,方便通过组件名称,从useComponentConfigStore仓库内拿到要渲染的组件实例
js 复制代码
  const { components } = useComponentsStore()

3.再获取useComponentConfigStore仓库内的componentConfig对象

  • 用于通过组件名来获取组件实例
js 复制代码
  const { componentConfig } = useComponentConfigStore()

4.编写renderComponents渲染函数(代码在下面)

5.最后在return内的div容器中调用renderComponents函数即可

js 复制代码
export default function EditArea() {
  const { componentConfig } = useComponentConfigStore()
  const { components } = useComponentsStore()
    //组件递归渲染
  function renderComponents(components: Component[]): React.ReactNode {//因为函数返回的是一个dom结构,所以使用React内置的ReactNode类型限制函数类型
    return components.map((component: Component) => {//拿到json数据进行遍历
      const config = componentConfig?.[component.name]//找到json数据中name在componentConfig中对应的组件
      if (!config?.component) {//如果当前名字没有对应的组件
        return null
      }
      //渲染组件
      return React.createElement(config.component, {
        key: component.id,//必须添加的key属性,表示该组件的唯一标识,属于react内部的特殊属性,在浏览器开发者工具上看不见,但是在react内部会使用这个属性来进行组件的识别和更新
        id: component.id,
        name: component.name,
        ...config.defaultProps,//组件的默认属性
        ...component.props,//组件的更新属性,会覆盖默认属性
      },
        renderComponents(component.children || [])//递归渲染整个json树,就是一直向下渲染它的子组件,知道没有为止
      )
    })
  }
  return (
    <div>
      {renderComponents(components )}
    </div>
  )
}

实现物料区组件的拖拽功能

1. 安装react-dnd ---用于跨组件传递数据,允许您定义可拖拽元素和放置目标,并在它们之间传递数据

css 复制代码
    npm i react-dnd  

2. 安装react-dnd-html5-backend--用于实现组件拖拽效果

css 复制代码
    npm i react-dnd-html5-backend

3.在main.tsx文件中进行引入,认其全局生效

js 复制代码
//main.txs中的部分代码
import {DndProvider} from 'react-dnd'
import {HTML5Backend} from 'react-dnd-html5-backend'
createRoot(document.getElementById('root')!).render(
    <DndProvider backend={HTML5Backend}>
        <App />
    </DndProvider>
)

4.将useComponentConfigStore仓库内打造的组件实例名称展示在左边的取料区

大体思路:将useComponentConfigStore仓库内打造的组件实例的名称加个边框,放在物料区,这里并不需要放置组件实例,只要将其拖拽至画布区后,才需要放置实例至画布区

js 复制代码
//Materail/index.tsx内完整代码
import { useComponentConfigStore } from '../../stores/component-config'
import { useMemo } from 'react'//用于缓存

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 => (
          <div
            className=' 
            border-dashed 
            border-[1px] 
            border-[#000] 
            py-[8px] px-[10px] 
            inline-block 
            bg-white
            m-[10px]
            cursor-pointer// 鼠标放到组件上,变为手型
            hover:bg-[#ccc]//鼠标放到组件上,组件背景颜色变为灰色
            '
            key={item.name}
          >
            {item.name}//组件名称
          </div>
        ))
      }
    </div>
  )
}

效果图

总结:

今天完成了以下操作:

  1. 创建了useComponentsStore仓库,用于存储整个json树
  2. 创建了Page,Container,Button组件
  3. 创建了useComponentConfigStore仓库,用于存储组件实例
  4. 打造了renderComponents渲染函数,用于渲染json树
  5. 将已创建的组件实例展示在了物料区,为拖拽做准备
相关推荐
fs哆哆几秒前
在VB.net中,函数:列数字转字母
java·服务器·前端·javascript·.net
Hilaku42 分钟前
别再手写i18n了!深入浏览器原生Intl对象(数字、日期、复数处理)
前端·javascript·代码规范
每天吃饭的羊1 小时前
强制缓存与协商缓存
前端
_一条咸鱼_1 小时前
LangChain跨会话记忆恢复技术源码解析(35)
人工智能·面试
缘来小哥1 小时前
Nodejs的多版本管理,不仅仅只是nvm的使用
前端·node.js
陈随易1 小时前
Vite和pnpm都在用的tinyglobby文件匹配库
前端·后端·程序员
LeeAt1 小时前
还在为移动端项目组件发愁?快来试试React Vant吧!
前端·web components
李剑一1 小时前
AI一定会淘汰程序员,并且已经开始淘汰程序员
人工智能·面试·程序员
_一条咸鱼_1 小时前
Android Runtime死代码消除原理深度剖析(93)
android·面试·android jetpack
鹏程十八少1 小时前
4. Android 用户狂赞的UI特效!揭秘折叠卡片+流光动画的终极实现方案
前端