frontend
26
总安装量
18
周安装量
#14542
全站排名
安装命令
npx skills add https://github.com/miles990/claude-software-skills --skill frontend
Agent 安装分布
opencode
13
claude-code
13
antigravity
9
gemini-cli
9
cursor
8
Skill 文档
Frontend Development
Overview
Modern frontend development patterns, frameworks, and best practices for building performant web applications.
React Ecosystem
Component Patterns
// Functional component with hooks
import { useState, useEffect, useCallback, useMemo } from 'react';
interface UserListProps {
initialFilter?: string;
onSelect: (user: User) => void;
}
function UserList({ initialFilter = '', onSelect }: UserListProps) {
const [users, setUsers] = useState<User[]>([]);
const [filter, setFilter] = useState(initialFilter);
const [loading, setLoading] = useState(true);
// Memoized filtered list
const filteredUsers = useMemo(
() => users.filter(u => u.name.toLowerCase().includes(filter.toLowerCase())),
[users, filter]
);
// Stable callback reference
const handleSelect = useCallback((user: User) => {
onSelect(user);
}, [onSelect]);
useEffect(() => {
async function fetchUsers() {
setLoading(true);
const data = await api.getUsers();
setUsers(data);
setLoading(false);
}
fetchUsers();
}, []);
if (loading) return <Skeleton count={5} />;
return (
<div>
<SearchInput value={filter} onChange={setFilter} />
{filteredUsers.map(user => (
<UserCard
key={user.id}
user={user}
onClick={() => handleSelect(user)}
/>
))}
</div>
);
}
Custom Hooks
// Data fetching hook
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const controller = new AbortController();
async function fetchData() {
try {
setLoading(true);
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const json = await response.json();
setData(json);
setError(null);
} catch (err) {
if (err instanceof Error && err.name !== 'AbortError') {
setError(err);
}
} finally {
setLoading(false);
}
}
fetchData();
return () => controller.abort();
}, [url]);
return { data, error, loading };
}
// Local storage hook
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = useCallback((value: T | ((val: T) => T)) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
}, [key, storedValue]);
return [storedValue, setValue] as const;
}
// Debounce hook
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
State Management
// Zustand - simple global state
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface CartStore {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
clearCart: () => void;
total: () => number;
}
const useCartStore = create<CartStore>()(
persist(
(set, get) => ({
items: [],
addItem: (item) => set((state) => ({
items: [...state.items, item]
})),
removeItem: (id) => set((state) => ({
items: state.items.filter(i => i.id !== id)
})),
clearCart: () => set({ items: [] }),
total: () => get().items.reduce((sum, item) => sum + item.price, 0)
}),
{ name: 'cart-storage' }
)
);
// React Query / TanStack Query
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: () => api.getUsers(),
staleTime: 5 * 60 * 1000, // 5 minutes
});
}
function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (newUser: CreateUserInput) => api.createUser(newUser),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
}
Next.js / App Router
Server Components
// app/users/page.tsx - Server Component (default)
async function UsersPage() {
// Direct database access in server component
const users = await db.user.findMany();
return (
<div>
<h1>Users</h1>
<UserList users={users} />
</div>
);
}
// Client component for interactivity
'use client';
import { useState } from 'react';
function UserList({ users }: { users: User[] }) {
const [selected, setSelected] = useState<string | null>(null);
return (
<ul>
{users.map(user => (
<li
key={user.id}
onClick={() => setSelected(user.id)}
className={selected === user.id ? 'selected' : ''}
>
{user.name}
</li>
))}
</ul>
);
}
Server Actions
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createUser(formData: FormData) {
const name = formData.get('name') as string;
const email = formData.get('email') as string;
await db.user.create({
data: { name, email }
});
revalidatePath('/users');
redirect('/users');
}
// Usage in component
function CreateUserForm() {
return (
<form action={createUser}>
<input name="name" required />
<input name="email" type="email" required />
<button type="submit">Create</button>
</form>
);
}
Route Handlers
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const limit = parseInt(searchParams.get('limit') || '10');
const users = await db.user.findMany({ take: limit });
return NextResponse.json(users);
}
export async function POST(request: NextRequest) {
const body = await request.json();
const user = await db.user.create({ data: body });
return NextResponse.json(user, { status: 201 });
}
CSS & Styling
Tailwind CSS
// Component with Tailwind
function Card({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm
hover:shadow-md transition-shadow duration-200
dark:bg-gray-800 dark:border-gray-700">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
{title}
</h2>
<div className="text-gray-600 dark:text-gray-300">
{children}
</div>
</div>
);
}
// With clsx/cn for conditional classes
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
function Button({
variant = 'primary',
size = 'md',
className,
...props
}: ButtonProps) {
return (
<button
className={cn(
'inline-flex items-center justify-center rounded-md font-medium',
'focus:outline-none focus:ring-2 focus:ring-offset-2',
{
'bg-blue-600 text-white hover:bg-blue-700': variant === 'primary',
'bg-gray-200 text-gray-900 hover:bg-gray-300': variant === 'secondary',
'px-3 py-1.5 text-sm': size === 'sm',
'px-4 py-2 text-base': size === 'md',
'px-6 py-3 text-lg': size === 'lg',
},
className
)}
{...props}
/>
);
}
CSS Modules
// Button.module.css
.button {
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-weight: 500;
}
.primary {
background-color: var(--color-primary);
color: white;
}
.secondary {
background-color: var(--color-gray-200);
color: var(--color-gray-900);
}
// Button.tsx
import styles from './Button.module.css';
function Button({ variant = 'primary', children }) {
return (
<button className={`${styles.button} ${styles[variant]}`}>
{children}
</button>
);
}
Performance Optimization
Code Splitting
// Dynamic imports
import dynamic from 'next/dynamic';
const HeavyChart = dynamic(() => import('./HeavyChart'), {
loading: () => <ChartSkeleton />,
ssr: false, // Client-only component
});
// React.lazy with Suspense
const Dashboard = lazy(() => import('./Dashboard'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Dashboard />
</Suspense>
);
}
Virtualization
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualList({ items }: { items: Item[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
});
return (
<div ref={parentRef} className="h-[400px] overflow-auto">
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
transform: `translateY(${virtualItem.start}px)`,
height: `${virtualItem.size}px`,
}}
>
{items[virtualItem.index].name}
</div>
))}
</div>
</div>
);
}
Image Optimization
import Image from 'next/image';
function ProductImage({ src, alt }: { src: string; alt: string }) {
return (
<Image
src={src}
alt={alt}
width={400}
height={300}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQ..."
sizes="(max-width: 768px) 100vw, 400px"
priority={false}
/>
);
}
Testing
Component Testing
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
describe('LoginForm', () => {
it('submits with valid credentials', async () => {
const onSubmit = vi.fn();
render(<LoginForm onSubmit={onSubmit} />);
await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com');
await userEvent.type(screen.getByLabelText(/password/i), 'password123');
await userEvent.click(screen.getByRole('button', { name: /sign in/i }));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123',
});
});
});
it('shows validation errors', async () => {
render(<LoginForm onSubmit={vi.fn()} />);
await userEvent.click(screen.getByRole('button', { name: /sign in/i }));
expect(await screen.findByText(/email is required/i)).toBeInTheDocument();
});
});
Related Skills
- [[testing-strategies]] – Frontend testing
- [[performance-optimization]] – Web performance
- [[ux-principles]] – User experience
Sharp Edgesï¼å¸¸è¦é·é±ï¼
éäºæ¯å端éç¼ä¸æå¸¸è¦ä¸ä»£å¹æé«çé¯èª¤
SE-1: useEffect ç¡éè¿´å
- å´é度: critical
- æ å¢: useEffect 䏿´æ° stateï¼è該 state 忝 dependencyï¼å°è´ç¡ééæ°æ¸²æ
- åå : ä¸äºè§£ React ç dependency æ©å¶ãobject/array ä½çº dependency
- çç:
- é é¢å¡æ»ææ¥µåº¦ç·©æ ¢
- ç覽å¨è¨æ¶é«æçºå¢é·
- Network tab 顯示大ééè¤è«æ±
- 檢測:
useEffect.*setState.*\].*\{|useEffect\(\(\).*fetch.*\[\] - è§£æ³: æ£ç¢ºè¨å® dependenciesãä½¿ç¨ useCallback/useMemoãèæ ®ç¨ useRef 追蹤å¼
SE-2: è¨æ¶é«æ´©æ¼ (Memory Leak)
- å´é度: high
- æ å¢: Component unmount å¾ä»æ subscriptionãtimer æ async æä½æ´æ° state
- åå : æ²ææ¸ ç effectãæ²æåæ¶é²è¡ä¸ç fetch
- çç:
- Console è¦åãCan’t perform state update on unmounted componentã
- æç¨ç¨å¼è¶ç¨è¶æ ¢
- åæé é¢å¾èè³æéç¾
- 檢測:
useEffect.*setInterval(?!.*return)|useEffect.*addEventListener(?!.*return.*remove)|useEffect.*subscribe(?!.*return) - è§£æ³: å¨ useEffect ä¸ return cleanup functionãä½¿ç¨ AbortController åæ¶ fetch
SE-3: Props Drilling å°ç
- å´é度: medium
- æ å¢: çºäºå³éè³æçµ¦æ·±å±¤ componentï¼ä¸é層éè¦å³éä¸éè¦ç props
- åå : æ²æä½¿ç¨ Context æçæ 管çãcomponent çµæ§è¨è¨ä¸ç¶
- çç:
- ä¿®æ¹ä¸å prop éè¦æ¹å 5+ å component
- ä¸é層 component æå¾å¤åªæ¯ãå³éãç props
- é£ä»¥è¿½è¹¤è³ææµå
- 檢測:
props\.\w+.*props\.\w+.*props\.\w+|{.*,.*,.*,.*,.*,.*}.*=> - è§£æ³: ä½¿ç¨ ContextãZustand/Reduxãæ Component Composition
SE-4: éæ©åªå (Premature Optimization)
- å´é度: medium
- æ å¢: 卿²ææè½å顿éåº¦ä½¿ç¨ useMemo/useCallback/React.memo
- åå : 誤解éäº hooks çç¨éãç²ç®ãåªåã
- çç:
- ç¨å¼ç¢¼å 滿 useMemo 使²ææè½æ¹å
- åèå çºé¡å¤çè¨æ¶é«åæ¯è¼æä½è®æ ¢
- ç¨å¼ç¢¼é£ä»¥é±è®
- 檢測:
useMemo\(\(\).*return.*\d+|useCallback\(\(\).*console|React\.memo\(.*\)(?!.*areEqual) - è§£æ³: å æ¸¬éæè½ãåªå¨ç¢ºå®æå顿åªåãçè§£ä½æä½¿ç¨éäºå·¥å ·
SE-5: ä¸å®å ¨ç HTML 渲æ
- å´é度: critical
- æ å¢: ä½¿ç¨ dangerouslySetInnerHTML æç´æ¥æ¸²æç¨æ¶è¼¸å ¥ç HTML
- åå : çºäºæ¸²æ rich textãä¸äºè§£ XSS 風éª
- çç:
- XSS æ»ææ¼æ´
- ç¨æ¶å¯ä»¥æ³¨å ¥æ¡æè ³æ¬
- ç¶²ç«è¢«ç¨æ¼é£éæç«åè³æ
- 檢測:
dangerouslySetInnerHTML|innerHTML.*=.*user|v-html.*user - è§£æ³: ä½¿ç¨ DOMPurify æ¶æ¯ã使ç¨å®å ¨ç Markdown 渲æå¨ãé¿å ç´æ¥æ¸²æç¨æ¶è¼¸å ¥
Validations
V-1: useEffect ç¼ºå° cleanup
- é¡å: regex
- å´é度: high
- 模å¼:
useEffect\s*\([^)]*=>\s*\{[^}]*(setInterval|addEventListener|subscribe)[^}]*\}(?![^}]*return) - è¨æ¯: useEffect with subscription/timer missing cleanup function
- 修復建è°: Add cleanup:
return () => clearInterval(id)orreturn () => unsubscribe() - é©ç¨:
*.tsx,*.jsx
V-2: ç¦æ¢ dangerouslySetInnerHTML
- é¡å: regex
- å´é度: critical
- 模å¼:
dangerouslySetInnerHTML - è¨æ¯: dangerouslySetInnerHTML is a security risk (XSS)
- 修復建è°: Use DOMPurify:
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} - é©ç¨:
*.tsx,*.jsx
V-3: useEffect 空 dependency array + state æ´æ°
- é¡å: regex
- å´é度: high
- 模å¼:
useEffect\s*\([^)]*=>\s*\{[^}]*set\w+\([^}]*\},\s*\[\s*\]\s*\) - è¨æ¯: useEffect with empty deps but updates state – possible stale closure
- 修復建è°: Add necessary dependencies or use useRef for values that shouldn’t trigger re-run
- é©ç¨:
*.tsx,*.jsx
V-4: ç¦æ¢ inline styles ç©ä»¶åé¢é
- é¡å: regex
- å´é度: medium
- 模å¼:
style=\{\s*\{[^}]+\}\s*\} - è¨æ¯: Inline style object creates new reference on every render
- 修復建è°: Extract to variable or use useMemo:
const styles = useMemo(() => ({...}), []) - é©ç¨:
*.tsx,*.jsx
V-5: ç¦æ¢å¨ JSX ä¸ä½¿ç¨ index ä½çº key
- é¡å: regex
- å´é度: medium
- 模å¼:
key=\{(index|i|idx)\} - è¨æ¯: Using array index as key can cause issues with reordering
- 修復建è°: Use a unique identifier:
key={item.id}orkey={item.uniqueField} - é©ç¨:
*.tsx,*.jsx