Local MCP Server
在Termux中运行本地MCP服务器,支持Ollama模型的文件读取与命令执行。
下载 11
一套完整的 React 生产级应用构建方法,涵盖架构、组件、状态管理与性能优化。
openclaw skills install @1kalin/afrexai-react-production命令、参数、文件名以原文为准
构建生产级 React 应用的完整方法论。涵盖架构决策、组件设计、状态管理、性能优化、测试与部署 —— 不仅是 API 参考,更包含决策框架、模板和评分系统。
any 类型(+2 分)project:
name: ""
type: "" # spa | ssr | hybrid | static
framework: "" # next | remix | vite-spa | astro
scale: "" # small (<20 routes) | medium (20-100) | large (100+)
team_size: "" # solo | small (2-5) | medium (6-15) | large (15+)
current_state:
react_version: "" # 18 | 19
typescript: true
router: "" # react-router | next-app | tanstack-router
state_management: "" # useState | zustand | jotai | redux | tanstack-query
styling: "" # tailwind | css-modules | styled-components | vanilla-extract
testing: "" # vitest | jest | playwright | cypress
ci_cd: "" # github-actions | gitlab-ci | vercel
pain_points: []
goals: []| 因素 | Vite SPA | Next.js | Remix | Astro |
|---|---|---|---|---|
| 需要 SEO | ❌ | ✅ 最佳 | ✅ 良好 | ✅ 最佳 |
| 仪表盘/应用类项目 | ✅ 最佳 | ✅ 良好 | ✅ 良好 | ❌ |
| 内容密集型 | ❌ | ✅ 良好 | ✅ 良好 | ✅ 最佳 |
| 团队熟悉度 | ✅ 简单 | ⚠️ 学习曲线 | ⚠️ Web 标准 | ⚠️ Islands |
| 部署灵活性 | 任意位置 | Vercel 最优 | 任意位置 | 任意位置 |
| 打包体积控制 | 完全可控 | 框架开销 | 更小 | 最小 JS |
决策规则:
src/
├── app/ # 路由/页面(框架相关)
├── features/ # 特征模块(核心模式)
│ ├── auth/
│ │ ├── components/ # 特征专属组件
│ │ ├── hooks/ # 特征专属自定义 Hook
│ │ ├── api/ # API 请求与类型定义
│ │ ├── utils/ # 特征专用工具函数
│ │ ├── types.ts # 特征类型定义
│ │ └── index.ts # 公共 API(导出聚合)
│ ├── dashboard/
│ └── settings/
├── shared/ # 跨特征共享代码
│ ├── components/ # 通用 UI 组件
│ │ ├── ui/ # 原语组件(Button, Input, Card)
│ │ └── layout/ # 布局组件
│ ├── hooks/ # 通用自定义 Hook
│ ├── lib/ # 工具函数、常量
│ └── types/ # 全局类型定义
├── providers/ # Context 提供者
└── styles/ # 全局样式features/ 目录之间禁止直接导入;应通过 shared/ 或事件通信index.ts 文件,定义其公共接口eslint-plugin-import 强制约束shared/types组件: PascalCase.tsx (UserProfile.tsx)
自定义 Hook: useCamelCase.ts (useAuth.ts)
工具函数: camelCase.ts (formatCurrency.ts)
类型定义: PascalCase.ts (User.ts)或 types.ts
常量: SCREAMING_SNAKE.ts (API_ENDPOINTS.ts)
测试文件: *.test.tsx (UserProfile.test.tsx)
故事文件: *.stories.tsx (Button.stories.tsx)// 1. 导入(分组:react → 第三方库 → 内部模块 → 类型 → 样式)
import { useState, useCallback, memo } from 'react'
import { clsx } from 'clsx'
import { Button } from '@/shared/components/ui'
import type { User } from '../types'
// 2. 类型定义(对外暴露,供复用)
export interface UserCardProps {
user: User
onEdit?: (id: string) => void
variant?: 'compact' | 'full'
className?: string
}
// 3. 组件(命名导出,非默认导出)
export const UserCard = memo(function UserCard({
user,
onEdit,
variant = 'full',
className,
}: UserCardProps) {
// 4. 自定义 Hook 优先
const [isExpanded, setIsExpanded] = useState(false)
// 5. 派生状态(不使用 useEffect 计算派生值!)
const displayName = `${user.firstName} ${user.lastName}`
// 6. 事件处理器(使用 useCallback 以避免不必要的重新创建)
const handleEdit = useCallback(() => {
onEdit?.(user.id)
}, [onEdit, user.id])
// 7. 边界情况提前返回
if (!user) return null
// 8. JSX(最多 50 行)
return (
<div className={clsx('rounded-lg border p-4', className)}>
<h3>{displayName}</h3>
{variant === 'full' && <p>{user.bio}</p>}
{onEdit && <Button onClick={handleEdit}>编辑</Button>}
</div>
)
})1. 复合组件(用于相关 UI 组)
// 使用方式: <Tabs><Tabs.List><Tabs.Tab>A</Tabs.Tab></Tabs.List><Tabs.Panel>...</Tabs.Panel></Tabs>
const TabsContext = createContext<TabsContextType | null>(null)
export function Tabs({ children, defaultValue }: TabsProps) {
const [activeTab, setActiveTab] = useState(defaultValue)
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
{children}
</TabsContext.Provider>
)
}
Tabs.List = TabsList
Tabs.Tab = TabsTab
Tabs.Panel = TabsPanel2. 渲染属性(用于灵活的渲染逻辑)
export function DataList<T>({ items, renderItem, renderEmpty }: DataListProps<T>) {
if (items.length === 0) return renderEmpty?.() ?? <EmptyState />
return <ul>{items.map((item, i) => <li key={i}>{renderItem(item)}</li>)}</ul>
}3. 高阶组件(用于跨切面关注点 — 建议谨慎使用)
export function withAuth<P>(Component: ComponentType<P>) {
return function AuthenticatedComponent(props: P) {
const { user, isLoading } = useAuth()
if (isLoading) return <Spinner />
if (!user) return <Navigate to="/login" />
return <Component {...props} />
}
}是否为服务器数据(来自 API)?
├─ 是 → 使用 TanStack Query(或 SWR)—— 服务器状态绝不使用 Redux/Zustand
│
└─ 否 → 是否在多个功能间共享?
├─ 是 → 是否复杂且涉及多种操作?
│ ├─ 是 → 使用 Zustand(或团队熟悉 Redux Toolkit)
│ └─ 否 → 使用 Jotai(原子化状态)或 Zustand(简单状态存储)
│
└─ 否 → 是否在单个功能内共享?
├─ 是 → 使用 Context + useReducer(或 Zustand 功能模块)
└─ 否 → 使用 useState / useReducer(组件本地状态)| 工具 | 适用场景 | 包大小 | 学习难度 | 团队规模 |
|---|---|---|---|---|
| useState | 组件本地状态 | 0 KB | 无 | 任意 |
| useReducer | 复杂本地状态 | 0 KB | 低 | 任意 |
| Context | 功能范围内的状态,更新频率低 | 0 KB | 低 | 任意 |
| Zustand | 全局客户端状态 | 1.1 KB | 低 | 任意 |
| Jotai | 原子化派生状态 | 3.4 KB | 中等 | 小型至中型 |
| TanStack Query | 服务器状态 | 12 KB | 中等 | 任意 |
| Redux Toolkit | 复杂全局状态 + 中间件 | 11 KB | 高 | 大型 |
// api/users.ts — 查询键工厂模式
export const userKeys = {
all: ['users'] as const,
lists: () => [...userKeys.all, 'list'] as const,
list: (filters: Filters) => [...userKeys.lists(), filters] as const,
details: () => [...userKeys.all, 'detail'] as const,
detail: (id: string) => [...userKeys.details(), id] as const,
}
// hooks/useUsers.ts
export function useUsers(filters: Filters) {
return useQuery({
queryKey: userKeys.list(filters),
queryFn: () => fetchUsers(filters),
staleTime: 5 * 60 * 1000, // 5 分钟
placeholderData: keepPreviousData,
})
}
export function useUpdateUser() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: updateUser,
onMutate: async (newUser) => {
// 乐观更新
await queryClient.cancelQueries({ queryKey: userKeys.detail(newUser.id) })
const previous = queryClient.getQueryData(userKeys.detail(newUser.id))
queryClient.setQueryData(userKeys.detail(newUser.id), newUser)
return { previous }
},
onError: (err, newUser, context) => {
queryClient.setQueryData(userKeys.detail(newUser.id), context?.previous)
},
onSettled: (data, err, variables) => {
queryClient.invalidateQueries({ queryKey: userKeys.detail(variables.id) })
queryClient.invalidateQueries({ queryKey: userKeys.lists() })
},
})
}// stores/useUIStore.ts — 聚焦、轻量的状态存储
interface UIStore {
sidebarOpen: boolean
theme: 'light' | 'dark' | 'system'
toggleSidebar: () => void
setTheme: (theme: UIStore['theme']) => void
}
export const useUIStore = create<UIStore>()(
persist(
(set) => ({
sidebarOpen: true,
theme: 'system',
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
setTheme: (theme) => set({ theme }),
}),
{ name: 'ui-preferences' }
)
)
// 使用方式:const theme = useUIStore((s) => s.theme) — 始终使用选择器!useStore(s => s.field) 而非 useStore()// hooks/useDebounce.ts
export function useDebounce<T>(value: T, delayMs: number = 300): T {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delayMs)
return () => clearTimeout(timer)
}, [value, delayMs])
return debouncedValue
}| Hook | 用途 | 使用场景 |
|---|---|---|
useDebounce | 防抖值变化 | 搜索输入框、窗口大小调整 |
useMediaQuery | 响应式断点检测 | 条件渲染 |
useLocalStorage | 本地持久化状态 | 用户偏好设置、草稿保存 |
useIntersection | 可见性检测 | 懒加载、无限滚动 |
usePrevious | 记录上一次值 | 动画效果、值对比 |
useClickOutside | 检测外部点击 | 下拉菜单、模态框 |
useEventListener | 安全绑定事件 | 键盘、滚动、窗口大小变化 |
useToggle | 布尔状态切换 | 模态框、折叠面板 |
useUserSearch 而非 useEverythinguseDebounce(value, { delay: 300 }) 更易扩展和维护useEffect 的返回函数中进行清理if 或 switch 中调用renderHook 进行单元测试{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true,
"forceConsistentCasingInFileNames": true,
"paths": {
"@/*": ["./src/*"]
}
}
}// 1. 区分式联合类型(用于状态机)
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error }
// 2. 多态组件(支持自定义标签)
type ButtonProps<C extends ElementType = 'button'> = {
as?: C
variant?: 'primary' | 'secondary'
} & ComponentPropsWithoutRef<C>
export function Button<C extends ElementType = 'button'>({
as,
variant = 'primary',
...props
}: ButtonProps<C>) {
const Component = as || 'button'
return <Component {...props} />
}
// 3. 品牌类型(用于区分 ID 类型)
type UserId = string & { __brand: 'UserId' }
type PostId = string & { __brand: 'PostId' }
// 4. 使用 Zod 进行运行时验证
const userSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
role: z.enum(['admin', 'user', 'viewer']),
})
type User = z.infer<typeof userSchema>any** — 改用 unknown 并逐步缩小范围,或使用泛型{ status: 'success'; data: T } 而非 { data?: T; error?: Error }userId 错误传入需要 postId 的位置satisfies 而非 as** — config satisfies Config 保留类型推断;as Config 会误导类型系统| 指标 | 目标值 | 测量方式 |
|---|---|---|
| 首次内容绘制(FCP) | < 1.8 秒 | Lighthouse |
| 最大内容绘制(LCP) | < 2.5 秒 | Lighthouse |
| 交互至下一帧(INP) | < 200 毫秒 | Lighthouse |
| 累积布局偏移(CLS) | < 0.1 | Lighthouse |
| 打包体积(压缩后) | < 200 KB | webpack-bundle-analyzer |
| 主线程 JavaScript 执行时间 | < 3 秒 | Chrome DevTools |
| 优先级 | 优化手段 | 影响力 | 工作量 |
|---|---|---|---|
| P0 | 路由级代码分割 | 🔴 高 | 低 |
| P0 | 图片优化(next/image、srcset) | 🔴 高 | 低 |
| P1 | 树摇(Tree Shaking,使用命名导入) | 🟡 中等 | 低 |
| P1 | 长列表虚拟化 | 🟡 中等 | 中等 |
| P1 | 对耗时操作进行防抖 | 🟡 中等 | 低 |
| P2 | 对昂贵组件使用 React.memo | 🟢 低至中等 | 低 |
| P2 | 对复杂计算使用 useMemo/useCallback | 🟢 低至中等 | 低 |
| P3 | 将重计算任务移至 Web Workers | 🟢 低 | 高 |
// 1. 路由级分割(Next.js 自动支持,React Router 需手动配置)
const Dashboard = lazy(() => import('./features/dashboard'))
const Settings = lazy(() => import('./features/settings'))
// 2. 组件级分割(大型组件)
const Chart = lazy(() => import('./components/Chart'))
const MarkdownEditor = lazy(() =>
import('./components/MarkdownEditor').then(m => ({ default: m.MarkdownEditor }))
)
// 3. 第三方库级分割(重型依赖)
const { PDFViewer } = await import('@react-pdf/renderer')// 启用 React Compiler 后,无需手动使用 useMemo/useCallback/useMemo
// 编译器自动进行记忆化处理:
// ❌ const memoized = useMemo(() => expensiveCalc(data), [data])
// ✅ const memoized = expensiveCalc(data) // 编译器自动处理
// 在 babel.config.js 中启用:
// plugins: [['babel-plugin-react-compiler', {}]]style={{ color: 'red' }} 会导致每次重新渲染<Layout><ExpensiveChild /></Layout>Math.random()// 三级错误边界:
// 1. 应用级(捕获所有错误,显示完整页面错误)
// 2. 功能级(隔离功能失败)
// 3. 组件级(用于高风险组件——图表、第三方)
// 使用现代错误边界库 react-error-boundary
import { ErrorBoundary, FallbackProps } from 'react-error-boundary'
function FeatureErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div role="alert" className="rounded-lg border-red-200 bg-red-50 p-4">
<h3>发生错误</h3>
<pre className="text-sm text-red-600">{error.message}</pre>
<button onClick={resetErrorBoundary}>重试</button>
</div>
)
}
// 使用方式:
<ErrorBoundary FallbackComponent={FeatureErrorFallback} onReset={() => queryClient.clear()}>
<DashboardFeature />
</ErrorBoundary>onError 或错误状态处理| 库 | 适用场景 | 包大小 | 渲染方式 |
|---|---|---|---|
| React Hook Form | 多数表单 | 9 KB | 最小化(非受控) |
| Formik | 简单表单 | 13 KB | 每次输入都渲染 |
| TanStack Form | 类型安全的复杂表单 | 5 KB | 受控 |
| 原生 HTML | 1-2 字段的小表单 | 0 KB | 完全由你控制 |
默认推荐:React Hook Form + Zod
const schema = z.object({
email: z.string().email('无效的邮箱'),
password: z.string().min(8, '至少 8 个字符'),
role: z.enum(['admin', 'user']),
})
type FormData = z.infer<typeof schema>
export function LoginForm({ onSubmit }: { onSubmit: (data: FormData) => void }) {
const form = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: { email: '', password: '', role: 'user' },
})
return (
<form onSubmit={form.handleSubmit(onSubmit)} noValidate>
<label htmlFor="email">邮箱</label>
<input id="email" type="email" {...form.register('email')} aria-invalid={!!form.formState.errors.email} />
{form.formState.errors.email && (
<p role="alert">{form.formState.errors.email.message}</p>
)}
{/* ... 更多字段 */}
<button type="submit" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? '登录中...' : '登录'}
</button>
</form>
)
}| 层级 | 工具 | 覆盖率目标 | 测试内容 |
|---|---|---|---|
| 单元测试 | Vitest | 80% 业务逻辑 | Hooks、工具函数、reducers |
| 组件测试 | Testing Library | 关键用户流程 | 渲染、交互、可访问性 |
| 集成测试 | Testing Library | 功能流程 | 多组件协作流程 |
| 端到端测试 | Playwright | 核心路径 | 登录、结账、核心流程 |
| 视觉测试 | Chromatic/Percy | UI 组件 | 回归检测 |
// 组件测试(Testing Library 原则:测试行为,而非实现)
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
describe('UserCard', () => {
it('点击编辑按钮时调用 onEdit 回调', async () => {
const user = userEvent.setup()
const onEdit = vi.fn()
render(<UserCard user={mockUser} onEdit={onEdit} />)
await user.click(screen.getByRole('button', { name: /编辑/i }))
expect(onEdit).toHaveBeenCalledWith(mockUser.id)
})
it('当未提供 onEdit 时不渲染编辑按钮', () => {
render(<UserCard user={mockUser} />)
expect(screen.queryByRole('button', { name: /编辑/i })).not.toBeInTheDocument()
})
})getByRole > getByTestId > getByTextuserEvent.click 模拟真实交互<button> 而非 <div onClick>,使用 <nav> 而非 <div class="nav"><img> 都有描述性 alt 属性(或 alt="" 表示装饰性图片)aria-label,提示信息使用 aria-describedbyaria-live="polite"prefers-reduced-motion 设置,避免不必要的动画vitest-axe 或 @axe-core/playwright)| 层级 | 推荐方案 | 备选方案 |
|---|---|---|
| 框架 | Next.js 15 | Remix、Vite SPA |
| 语言 | TypeScript(严格模式) | — |
| 样式 | Tailwind CSS v4 | CSS Modules |
| 组件库 | shadcn/ui | Radix、Headless UI |
| 状态管理(服务端) | TanStack Query v5 | SWR |
| 状态管理(客户端) | Zustand | Jotai |
| 表单处理 | React Hook Form + Zod | TanStack Form |
| 测试框架 | Vitest + Testing Library | Jest |
| 端到端测试 | Playwright | Cypress |
| 代码规范 | Biome | ESLint + Prettier |
| 认证系统 | Auth.js(NextAuth) | Clerk、Lucia |
| 数据库 | Drizzle ORM | Prisma |
| 部署平台 | Vercel | Cloudflare、Fly.io |
| 监控工具 | Sentry | Datadog |
| 维度 | 权重 | 评分内容 |
|---|---|---|
| 架构设计 | 20% | 结构合理性、分层清晰度、设计模式应用 |
| 类型安全 | 15% | 严格 TypeScript,禁止使用 any,Zod 边界校验 |
| 性能表现 | 15% | 核心网页指标(Core Web Vitals)、包体积控制 |
| 测试质量 | 15% | 测试覆盖率、测试质量、测试金字塔结构 |
| 无障碍访问 | 10% | 符合 WCAG AA 标准,支持键盘操作与屏幕阅读器 |
| 状态管理 | 10% | 工具选择合理,避免深层属性传递(prop drilling) |
| 错误处理 | 10% | 设置错误边界,用户友好提示,集成监控 |
| 开发者体验 | 5% | 代码格式化、静态检查、CI 执行速度 |
评分等级:
90+:世界级水平|75-89:生产就绪|60-74:需改进|<60:技术债危机
| # | 错误点 | 修复建议 |
|---|---|---|
| 1 | 在 useEffect 中计算派生状态 | 改用内联计算或 useMemo |
| 2 | 属性传递深度超过 5 层 | 使用 Context、Zustand 或组合模式 |
| 3 | 在 useEffect 中发起数据请求 | 使用 TanStack Query 或框架数据加载机制 |
| 4 | 大量使用默认导出 | 优先使用命名导出以提升重构安全性 |
| 5 | 测试实现细节而非行为 | 使用 Testing Library 测试用户行为 |
| 6 | 单个组件代码超过 500 行 | 拆分为独立的 hooks 和子组件 |
| 7 | 缺少错误边界 | 在应用层、功能层和组件层添加错误边界 |
| 8 | 用 Redux 管理服务端状态 | 使用 TanStack Query 管理 API 数据 |
| 9 | 将无障碍访问留到最后才处理 | 从第一天起就构建可访问性 |
| 10 | 未开启 TypeScript 严格模式 | 启用 strict 模式并解决所有错误 |
本技能提供系统化方法论。如需行业特定实现模式,可获取 AfrexAI 上下文包($47):
👉 浏览全部 10 个上下文包: https://afrexai-cto.github.io/context-packs/
afrexai-nextjs-production — Next.js 生产工程实践afrexai-vibe-coding — AI 辅助开发方法论afrexai-technical-seo — React SPA 与 SSR 的技术 SEOafrexai-test-automation-engineering — 完整的测试策略afrexai-ui-design-system — 设计系统架构已收录 10 个 Skill