核心理念:分层增强的组件架构
渐进式组件设计遵循”从基础到增强”的分层原则,确保组件在不同环境下都能提供适当的用户体验。
组件分层架构
第一层:原生 HTML 基础层
// 最基础的、无样式的原生组件
export function NativeButton({
type = 'button',
disabled,
children,
...props
}) {
return (
<button
type={type}
disabled={disabled}
{...props}
>
{children}
</button>
);
}
export function NativeInput({
type = 'text',
required,
disabled,
...props
}) {
return (
<input
type={type}
required={required}
disabled={disabled}
{...props}
/>
);
}特点:
- 零依赖,零运行时
- 完全可访问性支持
- 在任何环境下都能正常工作
- 提供最基础的功能性
第二层:视觉样式增强层
// 添加设计系统的视觉规范
export function StyledButton({
variant = 'primary',
size = 'medium',
children,
...props
}) {
const baseClasses = 'btn focus:outline-none transition-colors';
const variantClasses = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
danger: 'bg-red-600 text-white hover:bg-red-700'
};
const sizeClasses = {
small: 'px-3 py-1 text-sm',
medium: 'px-4 py-2',
large: 'px-6 py-3 text-lg'
};
return (
<NativeButton
{...props}
className={`
${baseClasses}
${variantClasses[variant]}
${sizeClasses[size]}
${props.className || ''}
`}
>
{children}
</NativeButton>
);
}第三层:交互行为增强层
// 添加丰富的交互反馈
export function InteractiveButton(props) {
const [isLoading, setIsLoading] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const handleClick = async (event) => {
if (props.disabled || isLoading) return;
setIsLoading(true);
try {
await props.onClick?.(event);
} catch (error) {
console.error('Button action failed:', error);
} finally {
setIsLoading(false);
}
};
return (
<StyledButton
{...props}
disabled={props.disabled || isLoading}
onClick={handleClick}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
aria-busy={isLoading}
style={{
transform: isHovered && !isLoading ? 'translateY(-1px)' : 'none',
transition: 'all 0.2s ease',
...props.style
}}
>
{isLoading ? (
<span className="flex items-center">
<Spinner size="small" />
<span className="ml-2">处理中...</span>
</span>
) : (
children
)}
</StyledButton>
);
}第四层:智能功能增强层
// 添加业务逻辑和高级功能
export function SmartButton({
analytics,
confirm,
undoable,
successFeedback,
...props
}) {
const [showUndo, setShowUndo] = useState(false);
const [undoTimer, setUndoTimer] = useState(null);
const handleClick = useCallback(async (event) => {
// 确认对话框
if (confirm && !window.confirm(confirm)) {
return;
}
// 数据分析
if (analytics) {
trackEvent('button_click', {
button_id: props.id,
...analytics
});
}
// 可撤销操作
if (undoable) {
setShowUndo(true);
const timer = setTimeout(() => setShowUndo(false), 5000);
setUndoTimer(timer);
try {
await props.onClick?.(event);
if (successFeedback) {
showToast(successFeedback);
}
} catch (error) {
setShowUndo(false);
clearTimeout(timer);
throw error;
}
return;
}
// 普通操作
try {
await props.onClick?.(event);
if (successFeedback) {
showToast(successFeedback);
}
} catch (error) {
showToast('操作失败,请重试');
throw error;
}
}, [props.onClick, confirm, analytics, undoable, successFeedback]);
const handleUndo = useCallback(() => {
setShowUndo(false);
if (undoTimer) clearTimeout(undoTimer);
// 执行撤销逻辑
performUndo();
}, [undoTimer]);
return (
<div className="flex items-center gap-2">
<InteractiveButton
{...props}
onClick={handleClick}
/>
{showUndo && (
<button
onClick={handleUndo}
className="text-sm text-blue-600 hover:text-blue-800"
>
撤销
</button>
)}
</div>
);
}表单组件的渐进式实现
基础表单结构
export function ProgressiveForm({ onSubmit, children, ...props }) {
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (event) => {
event.preventDefault();
setIsSubmitting(true);
try {
await onSubmit?.(event);
} finally {
setIsSubmitting(false);
}
};
return (
<form
{...props}
onSubmit={handleSubmit}
noValidate // 允许自定义验证
>
<fieldset disabled={isSubmitting}>
{children}
</fieldset>
</form>
);
}渐进式输入组件
export function ProgressiveInput({
validation,
realtimeValidation,
suggestions,
...props
}) {
const [errors, setErrors] = useState([]);
const [showSuggestions, setShowSuggestions] = useState(false);
// 基础验证
const validate = useCallback((value) => {
if (!validation) return [];
const newErrors = [];
if (validation.required && !value.trim()) {
newErrors.push('此字段为必填项');
}
if (validation.minLength && value.length < validation.minLength) {
newErrors.push(`至少需要${validation.minLength}个字符`);
}
if (validation.pattern && !validation.pattern.test(value)) {
newErrors.push(validation.message || '格式不正确');
}
return newErrors;
}, [validation]);
const handleChange = (event) => {
const value = event.target.value;
props.onChange?.(event);
// 实时验证
if (realtimeValidation) {
setErrors(validate(value));
}
// 显示建议
if (suggestions && value.length > 2) {
setShowSuggestions(true);
} else {
setShowSuggestions(false);
}
};
const handleBlur = (event) => {
// 失焦时验证
setErrors(validate(event.target.value));
setShowSuggestions(false);
props.onBlur?.(event);
};
return (
<div className="relative">
<NativeInput
{...props}
onChange={handleChange}
onBlur={handleBlur}
aria-invalid={errors.length > 0}
className={`
${props.className || ''}
${errors.length > 0 ? 'border-red-500' : 'border-gray-300'}
`}
/>
{/* 错误提示 */}
{errors.length > 0 && (
<div className="mt-1 text-sm text-red-600">
{errors.map((error, index) => (
<div key={index}>{error}</div>
))}
</div>
)}
{/* 智能建议 */}
{showSuggestions && suggestions && (
<div className="absolute z-10 w-full mt-1 bg-white border rounded shadow-lg">
{suggestions
.filter(suggestion =>
suggestion.toLowerCase().includes(props.value.toLowerCase())
)
.map((suggestion, index) => (
<div
key={index}
className="px-3 py-2 cursor-pointer hover:bg-gray-100"
onClick={() => {
props.onChange?.({
target: { value: suggestion }
});
setShowSuggestions(false);
}}
>
{suggestion}
</div>
))
}
</div>
)}
</div>
);
}组件能力检测与渐进加载
运行时能力检测
export function AdaptiveComponent({
fallback,
children,
requiredFeatures = []
}) {
const [canRender, setCanRender] = useState(true);
const [supportedFeatures, setSupportedFeatures] = useState({});
useEffect(() => {
// 检测浏览器支持的特性
const features = {
intersectionObserver: 'IntersectionObserver' in window,
webGL: detectWebGLSupport(),
touchEvents: 'ontouchstart' in window,
serviceWorker: 'serviceWorker' in navigator,
};
setSupportedFeatures(features);
// 检查是否满足渲染条件
const hasRequiredFeatures = requiredFeatures.every(
feature => features[feature]
);
setCanRender(hasRequiredFeatures);
}, [requiredFeatures]);
if (!canRender) {
return fallback ? fallback(supportedFeatures) : null;
}
return children(supportedFeatures);
}使用示例
function App() {
return (
<div>
{/* 基础组件 - 始终渲染 */}
<StyledButton variant="primary">
基础按钮
</StyledButton>
{/* 增强组件 - 条件渲染 */}
<AdaptiveComponent
requiredFeatures={['webGL', 'intersectionObserver']}
fallback={(features) => (
<div>
<p>您的设备不支持 3D 预览功能</p>
<img src="/static-preview.jpg" alt="产品预览" />
</div>
)}
>
{(features) => (
<WebGLViewer
model="/model.glb"
enableIntersection={features.intersectionObserver}
/>
)}
</AdaptiveComponent>
</div>
);
}设计系统主题的渐进式应用
主题分层系统
// 1. 基础主题变量
export const baseTheme = {
colors: {
text: '#000000',
background: '#ffffff',
primary: '#007acc',
error: '#d32f2f',
warning: '#ff9800',
success: '#4caf50'
},
spacing: {
xs: '0.25rem',
sm: '0.5rem',
md: '1rem',
lg: '1.5rem',
xl: '2rem'
}
};
// 2. 组件特定主题
export const componentTheme = {
button: {
borderRadius: '0.375rem',
borderWidth: '1px',
fontSize: {
small: '0.875rem',
medium: '1rem',
large: '1.125rem'
}
}
};
// 3. 品牌定制主题
export const createBrandTheme = (brandColors) => ({
...baseTheme,
colors: {
...baseTheme.colors,
...brandColors
}
});测试策略
分层测试架构
// 1. 基础层测试 - 功能性和可访问性
describe('NativeButton', () => {
it('renders with proper accessibility attributes', () => {
render(<NativeButton disabled>Click me</NativeButton>);
expect(screen.getByRole('button')).toBeDisabled();
});
});
// 2. 样式层测试 - 视觉一致性
describe('StyledButton', () => {
it('applies correct variant styles', () => {
render(<StyledButton variant="danger" />);
expect(screen.getByRole('button')).toHaveClass('bg-red-600');
});
});
// 3. 交互层测试 - 用户交互行为
describe('InteractiveButton', () => {
it('shows loading state during async operation', async () => {
render(<InteractiveButton onClick={() => new Promise(r => setTimeout(r, 100))} />);
fireEvent.click(screen.getByRole('button'));
expect(await screen.findByText('处理中...')).toBeInTheDocument();
});
});
// 4. 智能层测试 - 业务逻辑
describe('SmartButton', () => {
it('shows confirmation dialog when confirm prop is set', () => {
window.confirm = jest.fn(() => false);
render(<SmartButton confirm="Are you sure?" />);
fireEvent.click(screen.getByRole('button'));
expect(window.confirm).toHaveBeenCalledWith('Are you sure?');
});
});最佳实践总结
- 分层设计:从原生 HTML 开始,逐层增强样式、交互和智能功能
- 向后兼容:确保每一层增强都不会破坏基础层功能
- 能力检测:根据运行时环境决定渲染哪些增强功能
- 优雅降级:当增强功能不可用时,自动回退到基础版本
- 性能优先:按需加载增强层,避免不必要的运行时开销
- 测试覆盖:为每一层组件编写相应的测试用例
这种渐进式组件设计方法确保了设计系统在不同环境下的鲁棒性,同时为现代化浏览器提供了丰富的用户体验。