预览一开,灵魂出窍!低代码平台的魔法剧场大揭秘🎩✨

引言

在低代码平台的世界里,预览功能就像"魔镜"一样,能让我们实时看到组件的"真容"。今天,我们就来揭开低代码平台预览模式的神秘面纱,看看它是如何让组件"动起来"的!(别眨眼,前方高能⚡️) → 代码仓库地址内附项目体验地址


需求分析

  • 实时渲染:我们希望拖一个组件到画布上,立刻能看到它的样子。
  • 属性/样式/事件联动:调整右侧设置区,组件外观和行为要同步变化。
  • 安全隔离:预览时要防止恶意代码影响主应用。
  • 模式切换:编辑/预览一键切换,开发体验要丝滑如奶茶(◕‿◕✿)。

技术方案

  1. 状态管理:用 zustand 管理组件树、当前组件、模式等核心状态。
  2. 组件注册与配置:通过配置中心统一管理所有物料组件的开发/预览实现。
  3. 动态渲染:根据模式动态渲染 dev/prod 组件,保证编辑和预览体验一致。
  4. 属性/样式联动:props/styles 变更实时同步到组件实例。

代码实现

1. 顶部 Header:模式切换的"遥控器"

使用场景:顶部栏提供"编辑/预览"一键切换,提升开发效率。

tsx 复制代码
import { Space, Button } from 'antd'
import { useComponentsStore } from '../../stores/components'

export default function Header() {
  const { mode, setMode } = useComponentsStore((state: any) => state)
  console.log(mode);
  return (
    <div className='w-[100%] h-[100%]'>
      <div className='h-[50px] flex justify-between items-center px-[20px]'>
        <div>低码编辑器</div>
        <Space>
          {
            mode === 'edit' && (
              <Button type="primary" onClick={() => setMode('preview')}>预览</Button>
            )
          }
          {
            mode === 'preview' && (
              <Button type="primary" onClick={() => setMode('edit')}>退出预览</Button>
            )
          }
        </Space>
      </div>
    </div>
  )
}

原理分析

  • 通过 useComponentsStore 获取和切换 mode,实现编辑/预览的无缝切换。
  • 预览模式下,所有组件只读,防止误操作。
  • 这就像遥控器上的"切歌键",一按就换频道,体验贼爽!😎

2. 组件状态管理:组件树的"大脑"

使用场景:统一管理所有组件的结构、属性、样式和当前模式。

tsx 复制代码
import { create } from 'zustand'
import type { CSSProperties } from 'react'

export interface Component {
  id: number,
  name: string,
  props: any,
  styles?: CSSProperties,
  desc: string,
  children?: Component[],
  parentId?: number
}
export interface State {
  components: Component[],
  curComponentId?: number | null,
  curComponent: Component | null,
  mode: 'edit' | 'preview',
}
export interface Action {
  addComponent: (component: any, parentId?: number) => void;
  deleteComponent: (componentId: number) => void;
  updateComponentProps: (componentId: number, props: any) => void;  // 更新组件属性
  setCurComponentId: (componentId: number) => void;
  updateComponentStyles: (componentId: number, styles: CSSProperties) => void;
  setMode: (mode: 'edit' | 'preview') => void;
}

export const useComponentsStore = create<State & Action>(
  (set, get) => ({
    // 数据
    components: [  // 整个项目的 json 树
      {
        id: 1,
        name: 'Page',
        props: {},
        desc: '页面'
      }
    ],
    curComponentId: null,
    curComponent: null,
    mode: 'edit',
    // 方法
    addComponent: (component, parentId) => {  // 本质上就是要将一个对象添加到另一个对象中
      set((state) => {
        if (parentId) {
          // 获取到父级对象
          const parentComponent = getComponentById(parentId, state.components)
          if (parentComponent) {
            parentComponent.children ? parentComponent.children.push(component) : parentComponent.children = [component]
          }
          component.parentId = parentId
          return {
            components: [...state.components]
          }
        }
        return {
          components: [...state.components, component]
        }
      })
    },
    deleteComponent: (componentId) => { // 在整个 json 对象中找到某一个子对象的 id 为 componentId,移除该子对象
      if (!componentId) return 
      // 找到组件
      const component = getComponentById(componentId, get().components)
      if (component?.parentId) { // 有父级
        const parentComponent = getComponentById(component.parentId, get().components)
        if (parentComponent) {
          parentComponent.children = parentComponent.children?.filter((item) => item.id !== componentId)
        }
        set({
          components: [...get().components]
        })
      }
    },
    updateComponentProps: (componentId, props) => {
      set((state) => {
        const component = getComponentById(componentId, state.components)
        if (component) {
          component.props = {...component.props, ...props}
          return {
            components: [...state.components]
          }
        }
        return {components: [...state.components]}
      })
    },
    setCurComponentId: (componentId) => {
      set((state) => ({
        curComponentId: componentId,
        curComponent: getComponentById(componentId, state.components)
      }))
    },
    updateComponentStyles: (componentId, styles) => {  // 更新组件样式
      set(state => {
        const component = getComponentById(componentId, state.components)
        if (component) {
          component.styles = {...component.styles, ...styles}
          return {
            components: [...state.components]
          }
        }
        return {
          components: [...state.components]
        }
      }) 
    },
    setMode: (mode) => {
      set({
        mode: mode
      })
    }
  })
)

export function getComponentById(id: number | null, components: Component[]): Component | null {
  if (!id) return null
  for (const component of components) {
    if (component.id === id) {
      return component
    }
    if (component.children && component.children.length > 0) { 
      const result = getComponentById(id, component.children)
      if (result) {
        return result
      }
    }
  }
  return null
}

原理分析

  • 组件树(components)记录了所有组件的层级和属性。
  • mode 控制当前是编辑还是预览。
  • 所有操作(增删改查)都通过 store 方法完成,保证数据一致性。
  • 就像"中控大脑",一声令下,组件齐刷刷变脸!🤖

3. 组件注册与配置中心:物料的"身份证"

使用场景:统一管理所有组件的开发/预览实现、默认属性、可编辑项。

tsx 复制代码
import { create } from "zustand";
import ContainerDev from "../materials/Container/dev";
import ContainerProd from "../materials/Container/prod";
import ButtonDev from "../materials/Button/dev";
import ButtonProd from "../materials/Button/prod";
import PageDev from "../materials/Page/dev";
import PageProd from "../materials/Page/prod";

export interface ComponentSetter {
  name: string;
  label: string;
  type: string;
  [key: string]: any;
}

export interface ComponentConfig {
  name: string;
  defaultProps: Record<string, any>;
  // component: any;
  desc: string;
  setter?: ComponentSetter[];
  stylesSetter?: ComponentSetter[];
  dev: any, // 开发模式下的组件
  prod: any, // 预览模式下的组件
}

export interface State {
  componentConfig: {[key: string]: ComponentConfig}
}

export interface Action {
  registerComponent: (name: string, componentConfig: ComponentConfig) => void
}

// 每一个名字对应的组件具体是哪一个
export const useComponentConfigStore = create<State & Action>(
  (set) => ({
    componentConfig: {
      Container: {
        name: 'Container',
        defaultProps: {},
        desc: '容器',
        dev: ContainerDev,
        prod: ContainerProd,
      },
      Button: {
        name: 'Button',
        defaultProps: {
          type: 'primary',
          text: '按钮'
        },
        desc: '按钮',
        dev: ButtonDev,
        prod: ButtonProd,
        setter: [
          {
            name: 'type',
            label: '按钮类型',
            type: 'select',
            options: [
              {
                label: '主要按钮',
                value: 'primary'
              },
              {
                label: '次要按钮',
                value: 'default'
              }
            ]
          },
          {
            name: 'text',
            label: '文本',
            type: 'input'
          }
        ],
        stylesSetter: [
          {
            name: 'width',
            label: '宽度',
            type: 'inputNumber'
          },
          {
            name: 'height',
            label: '高度',
            type: 'inputNumber'
          }
        ]
      },
      Page: {
        name: 'Page',
        defaultProps: {},
        desc: '页面',
        dev: PageDev,
        prod: PageProd,
      }
    },

    registerComponent: (name, componentConfig) => {  
      set((state) => {
        return {
          ...state,
          componentConfig: {
            ...state.componentConfig,
            [name]: componentConfig
          }
        }
      })
    }
  })
)

原理分析

  • 每个组件有自己的"身份证",包括开发/预览实现、默认属性、可编辑项。
  • 通过配置中心,动态渲染不同模式下的组件。
  • 这就像"身份证+说明书",让每个组件都能被正确识别和使用!📇

4. 预览模式下的组件渲染

使用场景:根据当前模式,动态渲染 prod 组件,实现真实预览。

tsx 复制代码
import type { CommonComponentProps } from '../../interface'

export default function Container({ children, styles }: CommonComponentProps) {
  return (
    <>
      <div  
      className='p-[20px]'
        style={styles}
      >
        {children}
      </div>
    </>
  )
}
tsx 复制代码
import { Button as AntdButton } from 'antd'
import type { CommonComponentProps } from '../../interface'

export default function Button({type, text, styles}: CommonComponentProps) {
  return (
    <AntdButton type={type} style={styles}>{text}</AntdButton>
  )
}
tsx 复制代码
import type { CommonComponentProps } from '../../interface'

export default function Page({ children, styles }: CommonComponentProps) {
    return (
        <div
            className='p-[20px] '
            style={styles}
        >
            {children}
        </div>
    )
}

原理分析

  • 预览模式下,渲染 prod 组件,保证和线上效果一致。
  • 属性和样式通过 props/styles 实时传递,所见即所得。
  • 组件像演员一样,换上"正装"登台亮相,观众(开发者)一目了然!🎭

页面效果:


总结

低代码平台的预览功能,是"所见即所得"体验的灵魂。我们通过状态管理、组件注册、动态渲染等技术手段,让组件在编辑和预览之间自由切换,开发效率和体验双提升!(◕‿◕)ノ゙

项目所有代码均已整理至以下仓库,方便大家查看与使用:

代码仓库地址内附项目体验地址

相关推荐
计算机毕设定制辅导-无忧学长16 分钟前
InfluxDB 集群部署与高可用方案(二)
java·linux·前端
袁煦丞21 分钟前
MongoDB数据存储界的瑞士军刀:cpolar内网穿透实验室第513号成功挑战
前端·程序员·远程工作
天才熊猫君1 小时前
npm 和 pnpm 的一些理解
前端
飞飞飞仔1 小时前
从 Cursor AI 到 Claude Code AI:我的辅助编程转型之路
前端
qb2 小时前
vue3.5.18源码:调试方式
前端·vue.js·架构
Spider_Man2 小时前
缓存策略大乱斗:让你的页面快到飞起!
前端·http·node.js
前端老鹰2 小时前
CSS overscroll-behavior:解决滚动穿透的 “边界控制” 专家
前端·css·html
一叶怎知秋2 小时前
【openlayers框架学习】九:openlayers中的交互类(select和draw)
前端·javascript·笔记·学习·交互
allenlluo2 小时前
浅谈Web Components
前端·javascript
Mintopia2 小时前
把猫咪装进 public/ 文件夹:Next.js 静态资源管理的魔幻漂流
前端·javascript·next.js