前言
最近接到一个需求大概内容是这样的:页面布局中各个模块需要是可拖拽的,可响应式的,即可拖动改变每个容器大小来实现各自模块的UI响应式。即各自模块有自己的 breakpoints 断点,例如:sm, md, lg, xl
Demo
分析
可拖拽和可响应式的这个用业界开源的库结合使用就可以:
react-draggable: 元素可拖拽
re-resizable: 元素可 resize
我们需要自己实现一个根据容器宽度进行断点的容器组件
假设我们设计的断点如下:
const breakpoints = [375, 800, 1200];
const breakpointNames = ['sm', 'md', 'lg', 'xl'];
- 当容器宽度小于 375 时,
bp="sm",显示小屏幕内容。 - 当宽度在 375 到 800 之间时,
bp="md",显示中屏幕内容。 - 当宽度在 800 到 1200 之间时,
bp="lg",显示大屏幕内容。 - 当宽度大于 1200 时,
bp="xl",显示超大屏幕内容。
这里计算容器的宽度需要用到 ResizeObserver
实现
最终代码如下:
import React, { useState, useEffect, useRef, Children, cloneElement, isValidElement } from 'react';
import PropTypes from 'prop-types';
// 定义断点名称
const breakpointNames = ['sm', 'md', 'lg', 'xl'];
// 根据宽度计算断点
const calculateBreakpoint = (width, breakpoints, breakpointNames) => {
for (let i = 0; i < breakpoints.length; i++) {
if (width < breakpoints[i]) {
return breakpointNames[i] || '';
}
}
return breakpointNames[breakpoints.length] || ''; // 超过最大断点
};
const ResponsiveContainer = ({ children, breakpoints, nested = false }) => {
const containerRef = useRef(null);
const [currentBreakpoint, setCurrentBreakpoint] = useState('');
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width } = entry.contentRect;
const newBreakpoint = calculateBreakpoint(width, breakpoints, breakpointNames);
if (newBreakpoint !== currentBreakpoint) {
setCurrentBreakpoint(newBreakpoint);
}
}
});
// 监听容器尺寸变化
resizeObserver.observe(container);
return () => {
// 清除监听
resizeObserver.disconnect();
};
}, [breakpoints, currentBreakpoint]);
// 递归为所有子组件注入 bp 属性
const injectBreakpoint = (children) => {
return Children.map(children, (child) => {
if (isValidElement(child)) {
// 如果子组件有嵌套子组件,递归处理
const childProps = {
bp: currentBreakpoint,
// 是否需要给 所有嵌套的子组件也传入 bp,否则只给第一级子组件传入 bp 属性
children: nested ? injectBreakpoint(child.props.children) : child.props.children,
};
return cloneElement(child, childProps);
}
return child; // 如果不是有效的 React 元素,直接返回
});
};
return (
<div ref={containerRef} className={`responsive-container ${currentBreakpoint}`}>
{injectBreakpoint(children)}
</div>
);
};
ResponsiveContainer.propTypes = {
breakpoints: PropTypes.arrayOf(PropTypes.number).isRequired,
children: PropTypes.node.isRequired,
};
export default ResponsiveContainer;
使用demo
import React from 'react';
import ResponsiveContainer from './ResponsiveContainer';
const NestedChild = ({ bp }) => {
return <p>嵌套子组件,当前断点: {bp}</p>;
};
const ChildComponent = ({ bp, children }) => {
return (
<div>
<h1>子组件,当前断点: {bp}</h1>
{bp === 'sm' && <p>小屏幕内容</p>}
{bp === 'md' && <p>中屏幕内容</p>}
{bp === 'lg' && <p>大屏幕内容</p>}
{bp === 'xl' && <p>超大屏幕内容</p>}
{children}
</div>
);
};
const App = () => {
return (
<ResponsiveContainer breakpoints={[375, 800, 1200]} nested>
<ChildComponent>
<NestedChild />
<div><NestedChild /></div>
</ChildComponent>
</ResponsiveContainer>
);
};
export default App;
总结
一个简单的响应式容器组件就实现了,子组件可以根据 bp 属性去渲染对应的响应式内容了