引言
在低代码平台的世界里,预览功能就像"魔镜"一样,能让我们实时看到组件的"真容"。今天,我们就来揭开低代码平台预览模式的神秘面纱,看看它是如何让组件"动起来"的!(别眨眼,前方高能⚡️) → 代码仓库地址内附项目体验地址
需求分析
- 实时渲染:我们希望拖一个组件到画布上,立刻能看到它的样子。
- 属性/样式/事件联动:调整右侧设置区,组件外观和行为要同步变化。
- 安全隔离:预览时要防止恶意代码影响主应用。
- 模式切换:编辑/预览一键切换,开发体验要丝滑如奶茶(◕‿◕✿)。
技术方案
- 状态管理:用 zustand 管理组件树、当前组件、模式等核心状态。
- 组件注册与配置:通过配置中心统一管理所有物料组件的开发/预览实现。
- 动态渲染:根据模式动态渲染 dev/prod 组件,保证编辑和预览体验一致。
- 属性/样式联动: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 实时传递,所见即所得。
- 组件像演员一样,换上"正装"登台亮相,观众(开发者)一目了然!🎭
页面效果:

总结
低代码平台的预览功能,是"所见即所得"体验的灵魂。我们通过状态管理、组件注册、动态渲染等技术手段,让组件在编辑和预览之间自由切换,开发效率和体验双提升!(◕‿◕)ノ゙
项目所有代码均已整理至以下仓库,方便大家查看与使用:
→ 代码仓库地址内附项目体验地址