frontend-ui-engineering
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFrontend UI Engineering
前端UI工程
Overview
概述
Build production-quality user interfaces that are accessible, performant, and visually polished. The goal is UI that looks like it was built by a design-aware engineer at a top company — not like it was generated by an AI. This means real design system adherence, proper accessibility, thoughtful interaction patterns, and no generic "AI aesthetic."
构建可访问、性能优异且视觉精美的生产级用户界面。目标是打造出如同顶尖公司中懂设计的工程师所构建的UI——而非AI生成的风格。这意味着要严格遵循真实的设计系统、保证良好的可访问性、采用深思熟虑的交互模式,并且摒弃通用的“AI审美”。
When to Use
适用场景
- Building new UI components or pages
- Modifying existing user-facing interfaces
- Implementing responsive layouts
- Adding interactivity or state management
- Fixing visual or UX issues
- 构建新的UI组件或页面
- 修改现有的面向用户界面
- 实现响应式布局
- 添加交互功能或状态管理
- 修复视觉或UX问题
Component Architecture
组件架构
File Structure
文件结构
Colocate everything related to a component:
src/components/
TaskList/
TaskList.tsx # Component implementation
TaskList.test.tsx # Tests
TaskList.stories.tsx # Storybook stories (if using)
use-task-list.ts # Custom hook (if complex state)
types.ts # Component-specific types (if needed)将与组件相关的所有内容放在一起:
src/components/
TaskList/
TaskList.tsx # 组件实现
TaskList.test.tsx # 测试文件
TaskList.stories.tsx # Storybook故事(如果使用)
use-task-list.ts # 自定义Hook(如果涉及复杂状态)
types.ts # 组件专属类型(如有需要)Component Patterns
组件模式
Prefer composition over configuration:
tsx
// Good: Composable
<Card>
<CardHeader>
<CardTitle>Tasks</CardTitle>
</CardHeader>
<CardBody>
<TaskList tasks={tasks} />
</CardBody>
</Card>
// Avoid: Over-configured
<Card
title="Tasks"
headerVariant="large"
bodyPadding="md"
content={<TaskList tasks={tasks} />}
/>Keep components focused:
tsx
// Good: Does one thing
export function TaskItem({ task, onToggle, onDelete }: TaskItemProps) {
return (
<li className="flex items-center gap-3 p-3">
<Checkbox checked={task.done} onChange={() => onToggle(task.id)} />
<span className={task.done ? 'line-through text-muted' : ''}>{task.title}</span>
<Button variant="ghost" size="sm" onClick={() => onDelete(task.id)}>
<TrashIcon />
</Button>
</li>
);
}Separate data fetching from presentation:
tsx
// Container: handles data
export function TaskListContainer() {
const { tasks, isLoading, error } = useTasks();
if (isLoading) return <TaskListSkeleton />;
if (error) return <ErrorState message="Failed to load tasks" retry={refetch} />;
if (tasks.length === 0) return <EmptyState message="No tasks yet" />;
return <TaskList tasks={tasks} />;
}
// Presentation: handles rendering
export function TaskList({ tasks }: { tasks: Task[] }) {
return (
<ul role="list" className="divide-y">
{tasks.map(task => <TaskItem key={task.id} task={task} />)}
</ul>
);
}优先使用组合而非配置:
tsx
// 推荐:可组合式
<Card>
<CardHeader>
<CardTitle>Tasks</CardTitle>
</CardHeader>
<CardBody>
<TaskList tasks={tasks} />
</CardBody>
</Card>
// 避免:过度配置
<Card
title="Tasks"
headerVariant="large"
bodyPadding="md"
content={<TaskList tasks={tasks} />}
/>保持组件职责单一:
tsx
// 推荐:只做一件事
export function TaskItem({ task, onToggle, onDelete }: TaskItemProps) {
return (
<li className="flex items-center gap-3 p-3">
<Checkbox checked={task.done} onChange={() => onToggle(task.id)} />
<span className={task.done ? 'line-through text-muted' : ''}>{task.title}</span>
<Button variant="ghost" size="sm" onClick={() => onDelete(task.id)}>
<TrashIcon />
</Button>
</li>
);
}将数据获取与展示分离:
tsx
// 容器组件:处理数据
export function TaskListContainer() {
const { tasks, isLoading, error } = useTasks();
if (isLoading) return <TaskListSkeleton />;
if (error) return <ErrorState message="Failed to load tasks" retry={refetch} />;
if (tasks.length === 0) return <EmptyState message="No tasks yet" />;
return <TaskList tasks={tasks} />;
}
// 展示组件:负责渲染
export function TaskList({ tasks }: { tasks: Task[] }) {
return (
<ul role="list" className="divide-y">
{tasks.map(task => <TaskItem key={task.id} task={task} />)}
</ul>
);
}State Management
状态管理
Choose the simplest approach that works:
Local state (useState) → Component-specific UI state
Lifted state → Shared between 2-3 sibling components
Context → Theme, auth, locale (read-heavy, write-rare)
URL state (searchParams) → Filters, pagination, shareable UI state
Server state (React Query, SWR) → Remote data with caching
Global store (Zustand, Redux) → Complex client state shared app-wideAvoid prop drilling deeper than 3 levels. If you're passing props through components that don't use them, introduce context or restructure the component tree.
选择最简单可行的方案:
本地状态(useState) → 组件专属UI状态
提升状态(Lifted state) → 在2-3个兄弟组件间共享
上下文(Context) → 主题、权限、语言环境(读多写少场景)
URL状态(searchParams) → 筛选器、分页、可分享的UI状态
服务端状态(React Query、SWR) → 带缓存的远程数据
全局状态库(Zustand、Redux) → 应用内共享的复杂客户端状态避免prop drilling超过3层。 如果你需要通过不使用这些props的组件来传递props,引入上下文或重构组件树。
Design System Adherence
遵循设计系统
Avoid the AI Aesthetic
摒弃AI审美
AI-generated UI has recognizable patterns. Avoid all of them:
| AI Default | Production Quality |
|---|---|
| Purple/indigo everything | Use the project's actual color palette |
| Excessive gradients | Flat or subtle gradients matching the design system |
| Rounded everything (rounded-2xl) | Consistent border-radius from the design system |
| Generic hero sections | Content-first layouts |
| Lorem ipsum-style copy | Realistic placeholder content |
| Oversized padding everywhere | Consistent spacing scale |
| Stock card grids | Purpose-driven layouts |
| Shadow-heavy design | Subtle or no shadows unless the design system specifies |
AI生成的UI有可识别的模式。要避免所有这些模式:
| AI默认风格 | 生产级质感风格 |
|---|---|
| 全是紫色/靛蓝色 | 使用项目实际的调色板 |
| 过度使用渐变 | 采用符合设计系统的扁平或细微渐变 |
| 所有元素都极度圆角(rounded-2xl) | 遵循设计系统中统一的圆角半径 |
| 通用的hero区域 | 以内容为核心的布局 |
| Lorem ipsum类占位文本 | 真实的占位内容 |
| 各处都有超大内边距 | 遵循统一的间距规范 |
| 通用卡片网格 | 有明确目的的布局 |
| 大量使用阴影 | 除非设计系统指定,否则使用细微阴影或无阴影 |
Spacing and Layout
间距与布局
Use a consistent spacing scale. Don't invent values:
css
/* Use the scale: 0.25rem increments (or whatever the project uses) */
/* Good */ padding: 1rem; /* 16px */
/* Good */ gap: 0.75rem; /* 12px */
/* Bad */ padding: 13px; /* Not on any scale */
/* Bad */ margin-top: 2.3rem; /* Not on any scale */使用统一的间距规范,不要随意自定义数值:
css
/* 使用规范:0.25rem递增(或项目采用的其他规范) */
/* 推荐 */ padding: 1rem; /* 16px */
/* 推荐 */ gap: 0.75rem; /* 12px */
/* 避免 */ padding: 13px; /* 不在任何规范内 */
/* 避免 */ margin-top: 2.3rem; /* 不在任何规范内 */Typography
排版
Respect the type hierarchy:
h1 → Page title (one per page)
h2 → Section title
h3 → Subsection title
body → Default text
small → Secondary/helper textDon't skip heading levels. Don't use heading styles for non-heading content.
遵循字体层级:
h1 → 页面标题(每页一个)
h2 → 章节标题
h3 → 子章节标题
body → 默认文本
small → 次要/辅助文本不要跳过标题层级。不要将标题样式用于非标题内容。
Color
颜色
- Use semantic color tokens: ,
text-primary,bg-surface— not raw hex valuesborder-default - Ensure sufficient contrast (4.5:1 for normal text, 3:1 for large text)
- Don't rely solely on color to convey information (use icons, text, or patterns too)
- 使用语义化颜色令牌:、
text-primary、bg-surface— 而非原始十六进制值border-default - 确保足够的对比度(普通文本4.5:1,大文本3:1)
- 不要仅依赖颜色来传递信息(同时使用图标、文本或图案)
Accessibility (WCAG 2.1 AA)
无障碍访问(WCAG 2.1 AA)
Every component must meet these standards:
每个组件都必须满足这些标准:
Keyboard Navigation
键盘导航
tsx
// Every interactive element must be keyboard accessible
<button onClick={handleClick}>Click me</button> // ✓ Focusable by default
<div onClick={handleClick}>Click me</div> // ✗ Not focusable
<div role="button" tabIndex={0} onClick={handleClick} // ✓ But prefer <button>
onKeyDown={e => e.key === 'Enter' && handleClick()}>
Click me
</div>tsx
// 所有交互元素都必须支持键盘访问
<button onClick={handleClick}>Click me</button> // ✓ 默认可获取焦点
<div onClick={handleClick}>Click me</div> // ✗ 无法获取焦点
<div role="button" tabIndex={0} onClick={handleClick} // ✓ 但优先使用<button>
onKeyDown={e => e.key === 'Enter' && handleClick()}>
Click me
</div>ARIA Labels
ARIA标签
tsx
// Label interactive elements that lack visible text
<button aria-label="Close dialog"><XIcon /></button>
// Label form inputs
<label htmlFor="email">Email</label>
<input id="email" type="email" />
// Or use aria-label when no visible label exists
<input aria-label="Search tasks" type="search" />tsx
// 为没有可见文本的交互元素添加标签
<button aria-label="Close dialog"><XIcon /></button>
// 为表单输入框添加标签
<label htmlFor="email">Email</label>
<input id="email" type="email" />
// 当没有可见标签时,使用aria-label
<input aria-label="Search tasks" type="search" />Focus Management
焦点管理
tsx
// Move focus when content changes
function Dialog({ isOpen, onClose }: DialogProps) {
const closeRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (isOpen) closeRef.current?.focus();
}, [isOpen]);
// Trap focus inside dialog when open
return (
<dialog open={isOpen}>
<button ref={closeRef} onClick={onClose}>Close</button>
{/* dialog content */}
</dialog>
);
}tsx
// 当内容变化时移动焦点
function Dialog({ isOpen, onClose }: DialogProps) {
const closeRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (isOpen) closeRef.current?.focus();
}, [isOpen]);
// 当对话框打开时,将焦点限制在内部
return (
<dialog open={isOpen}>
<button ref={closeRef} onClick={onClose}>Close</button>
{/* 对话框内容 */}
</dialog>
);
}Meaningful Empty and Error States
有意义的空状态和错误状态
tsx
// Don't show blank screens
function TaskList({ tasks }: { tasks: Task[] }) {
if (tasks.length === 0) {
return (
<div role="status" className="text-center py-12">
<TasksEmptyIcon className="mx-auto h-12 w-12 text-muted" />
<h3 className="mt-2 text-sm font-medium">No tasks</h3>
<p className="mt-1 text-sm text-muted">Get started by creating a new task.</p>
<Button className="mt-4" onClick={onCreateTask}>Create Task</Button>
</div>
);
}
return <ul role="list">...</ul>;
}tsx
// 不要显示空白屏幕
function TaskList({ tasks }: { tasks: Task[] }) {
if (tasks.length === 0) {
return (
<div role="status" className="text-center py-12">
<TasksEmptyIcon className="mx-auto h-12 w-12 text-muted" />
<h3 className="mt-2 text-sm font-medium">No tasks</h3>
<p className="mt-1 text-sm text-muted">Get started by creating a new task.</p>
<Button className="mt-4" onClick={onCreateTask}>Create Task</Button>
</div>
);
}
return <ul role="list">...</ul>;
}Responsive Design
响应式设计
Design for mobile first, then expand:
tsx
// Tailwind: mobile-first responsive
<div className="
grid grid-cols-1 /* Mobile: single column */
sm:grid-cols-2 /* Small: 2 columns */
lg:grid-cols-3 /* Large: 3 columns */
gap-4
">Test at these breakpoints: 320px, 768px, 1024px, 1440px.
先为移动端设计,再扩展到桌面端:
tsx
// Tailwind:移动端优先的响应式设计
<div className="
grid grid-cols-1 /* 移动端:单列 */
sm:grid-cols-2 /* 小屏:2列 */
lg:grid-cols-3 /* 大屏:3列 */
gap-4
">在以下断点测试:320px、768px、1024px、1440px。
Loading and Transitions
加载与过渡
tsx
// Skeleton loading (not spinners for content)
function TaskListSkeleton() {
return (
<div className="space-y-3" aria-busy="true" aria-label="Loading tasks">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="h-12 bg-muted animate-pulse rounded" />
))}
</div>
);
}
// Optimistic updates for perceived speed
function useToggleTask() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: toggleTask,
onMutate: async (taskId) => {
await queryClient.cancelQueries({ queryKey: ['tasks'] });
const previous = queryClient.getQueryData(['tasks']);
queryClient.setQueryData(['tasks'], (old: Task[]) =>
old.map(t => t.id === taskId ? { ...t, done: !t.done } : t)
);
return { previous };
},
onError: (_err, _taskId, context) => {
queryClient.setQueryData(['tasks'], context?.previous);
},
});
}tsx
// 骨架屏加载(内容加载不要使用加载动画)
function TaskListSkeleton() {
return (
<div className="space-y-3" aria-busy="true" aria-label="Loading tasks">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="h-12 bg-muted animate-pulse rounded" />
))}
</div>
);
}
// 乐观更新提升感知速度
function useToggleTask() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: toggleTask,
onMutate: async (taskId) => {
await queryClient.cancelQueries({ queryKey: ['tasks'] });
const previous = queryClient.getQueryData(['tasks']);
queryClient.setQueryData(['tasks'], (old: Task[]) =>
old.map(t => t.id === taskId ? { ...t, done: !t.done } : t)
);
return { previous };
},
onError: (_err, _taskId, context) => {
queryClient.setQueryData(['tasks'], context?.previous);
},
});
}Common Rationalizations
常见自我辩解与实际情况
| Rationalization | Reality |
|---|---|
| "Accessibility is a nice-to-have" | It's a legal requirement in many jurisdictions and an engineering quality standard. |
| "We'll make it responsive later" | Retrofitting responsive design is 3x harder than building it from the start. |
| "The design isn't final, so I'll skip styling" | Use the design system defaults. Unstyled UI creates a broken first impression for reviewers. |
| "This is just a prototype" | Prototypes become production code. Build the foundation right. |
| "The AI aesthetic is fine for now" | It signals low quality. Use the project's actual design system from the start. |
| 自我辩解 | 实际情况 |
|---|---|
| “无障碍访问是锦上添花的功能” | 在许多地区这是法律要求,同时也是工程质量标准。 |
| “我们之后再做响应式适配” | 后期改造响应式设计的难度是从一开始就构建的3倍。 |
| “设计还没定稿,所以我先不做样式” | 使用设计系统的默认样式。无样式的UI会给评审者留下糟糕的第一印象。 |
| “这只是个原型” | 原型往往会变成生产代码。从一开始就打好基础。 |
| “AI审美现在凑合用就行” | 这会传递出低质量的信号。从一开始就使用项目实际的设计系统。 |
Red Flags
危险信号
- Components with more than 200 lines (split them)
- Inline styles or arbitrary pixel values
- Missing error states, loading states, or empty states
- No keyboard navigation testing
- Color as the sole indicator of state (red/green without text or icons)
- Generic "AI look" (purple gradients, oversized cards, stock layouts)
- 组件代码超过200行(拆分它)
- 内联样式或任意像素值
- 缺少错误状态、加载状态或空状态
- 未测试键盘导航
- 仅用颜色表示状态(红/绿但无文本或图标辅助)
- 通用的“AI风格”(紫色渐变、超大卡片、通用布局)
Verification
验证
After building UI:
- Component renders without console errors
- All interactive elements are keyboard accessible (Tab through the page)
- Screen reader can convey the page's content and structure
- Responsive: works at 320px, 768px, 1024px, 1440px
- Loading, error, and empty states all handled
- Follows the project's design system (spacing, colors, typography)
- No accessibility warnings in dev tools or axe-core
构建完UI后:
- 组件渲染时控制台无错误
- 所有交互元素都可通过键盘访问(按Tab键遍历页面)
- 屏幕阅读器可以传达页面的内容和结构
- 响应式:在320px、768px、1024px、1440px尺寸下均可正常工作
- 已处理加载、错误和空状态
- 遵循项目的设计系统(间距、颜色、排版)
- 开发工具或axe-core中无无障碍访问警告