关于 vercel-development
为使用 Vercel 构建和部署应用的团队提供开发指引,重点涉及 Next.js、React Server Components 和 Vercel AI SDK,也涵盖性能、配置与部署。
主要功能
- 01Next.js App Router 指引
- 02React Server Components 实践
- 03TypeScript 与 API 路由模式
- 04Vercel AI SDK 集成
- 05性能与无障碍指引
- 06Vercel 配置与部署
适用场景
- 01规划 Next.js 应用结构
- 02构建流式聊天界面
- 03优化应用性能
- 04准备 Vercel 部署
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ mindrally/ skills --skill vercel-development安装后在 Claude Code 中输入/vercel-development确认已加载。
详细介绍
Vercel 开发最佳实践
概述
本技能提供在 Vercel 上开发和部署应用的全面指南,重点涵盖 Next.js、React Server Components、Edge Functions 和 Vercel AI SDK。
核心原则
- 编写简洁、技术准确的 TypeScript 代码和示例
- 使用函数式和声明式编程模式;避免使用类
- 尽量减少 'use client'、'useEffect' 和 'setState' 的使用;优先采用 React Server Components(RSC)
- 使用 Tailwind CSS 实现响应式设计,并采用移动优先的方法
- 针对 Core Web Vitals 和性能进行优化
项目结构
my-app/
├── app/ # App Router pages and layouts
│ ├── (auth)/ # Route groups
│ ├── api/ # API routes
│ ├── layout.tsx # Root layout
│ └── page.tsx # Home page
├── components/ # React components
│ ├── ui/ # UI primitives
│ └── features/ # Feature components
├── lib/ # Utility functions
├── hooks/ # Custom React hooks
├── types/ # TypeScript types
├── public/ # Static assets
└── vercel.json # Vercel configuration
Next.js App Router 指南
文件命名约定
- 目录名称使用小写字母和连字符(例如,
components/auth-wizard) - 组件和函数优先使用具名导出
- 路由页面使用
page.tsx,布局使用layout.tsx - 加载状态使用
loading.tsx,错误边界使用error.tsx
服务端组件(默认)
// app/users/page.tsx
import { getUsers } from '@/lib/data';
export default async function UsersPage() {
const users = await getUsers();
return (
<main>
<h1>Users</h1>
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</main>
);
}
客户端组件(按需使用)
'use client';
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
);
}
TypeScript 标准
类型定义
// Use interfaces over types for object shapes
interface User {
id: string;
name: string;
email: string;
createdAt: Date;
}
// Use types for unions and complex types
type Status = 'pending' | 'active' | 'inactive';
// Avoid enums; use const objects instead
const STATUS = {
PENDING: 'pending',
ACTIVE: 'active',
INACTIVE: 'inactive',
} as const;
type StatusValue = typeof STATUS[keyof typeof STATUS];
组件 Props
interface ButtonProps {
children: React.ReactNode;
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
onClick?: () => void;
}
export function Button({
children,
variant = 'primary',
size = 'md',
disabled = false,
onClick,
}: ButtonProps) {
// Implementation
}
API 路由
路由处理程序
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
const CreateUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
export async function GET(request: NextRequest) {
const users = await getUsers();
return NextResponse.json(users);
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const validated = CreateUserSchema.parse(body);
const user = await createUser(validated);
return NextResponse.json(user, { status: 201 });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Validation failed', details: error.errors },
{ status: 400 }
);
}
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
Edge Runtime
// app/api/edge-function/route.ts
export const runtime = 'edge';
export async function GET(request: Request) {
return new Response(JSON.stringify({ message: 'Hello from the edge!' }), {
headers: { 'Content-Type': 'application/json' },
});
}
Vercel AI SDK 集成
流式聊天界面
'use client';
import { useChat } from 'ai/react';
export function Chat() {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
api: '/api/chat',
});
return (
<div className="flex flex-col h-full">
<div className="flex-1 overflow-y-auto">
{messages.map(message => (
<div key={message.id} className={message.role === 'user' ? 'text-right' : ''}>
<p>{message.content}</p>
</div>
))}
</div>
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={handleInputChange}
placeholder="Type a message..."
disabled={isLoading}
/>
</form>
</div>
);
}
AI API 路由
// app/api/chat/route.ts
import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';
export async function POST(request: Request) {
const { messages } = await request.json();
const result = await streamText({
model: openai('gpt-4-turbo'),
messages,
system: 'You are a helpful assistant.',
});
return result.toDataStreamResponse();
}
AI 错误处理
import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';
export async function POST(request: Request) {
try {
const { messages } = await request.json();
const result = await streamText({
model: openai('gpt-4-turbo'),
messages,
});
return result.toDataStreamResponse();
} catch (error) {
// Handle rate limiting
if (error.message?.includes('rate limit')) {
return new Response('Rate limit exceeded. Please try again later.', {
status: 429,
});
}
// Handle quota exceeded
if (error.message?.includes('quota')) {
return new Response('API quota exceeded.', { status: 402 });
}
// Fallback to alternative model
console.error('Primary model failed:', error);
return new Response('Service temporarily unavailable.', { status: 503 });
}
}
数据获取
服务端数据获取
// Fetch data in Server Components
async function getData() {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 }, // Cache for 1 hour
});
if (!res.ok) {
throw new Error('Failed to fetch data');
}
return res.json();
}
export default async function Page() {
const data = await getData();
return <div>{/* Render data */}</div>;
}
URL 状态管理
// Use URL query parameters for server state
import { useSearchParams, useRouter } from 'next/navigation';
export function Filters() {
const searchParams = useSearchParams();
const router = useRouter();
const updateFilter = (key: string, value: string) => {
const params = new URLSearchParams(searchParams);
params.set(key, value);
router.push(`?${params.toString()}`);
};
return (/* Filter UI */);
}
性能优化
图像优化
import Image from 'next/image';
export function Hero() {
return (
<Image
src="/hero.jpg"
alt="Hero image"
width={1200}
height={600}
priority // Load immediately for LCP
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>
);
}
动态导入
import dynamic from 'next/dynamic';
// Lazy load heavy components
const HeavyChart = dynamic(() => import('@/components/heavy-chart'), {
loading: () => <div>Loading chart...</div>,
ssr: false, // Disable SSR if needed
});
Suspense 边界
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<Loading />}>
<AsyncComponent />
</Suspense>
</div>
);
}
错误处理
错误边界
// app/error.tsx
'use client';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
);
}
全局错误处理程序
// app/global-error.tsx
'use client';
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</body>
</html>
);
}
Vercel 配置
vercel.json
{
"framework": "nextjs",
"regions": ["iad1"],
"crons": [
{
"path": "/api/cron/cleanup",
"schedule": "0 0 * * *"
}
],
"headers": [
{
"source": "/api/(.*)",
"headers": [
{ "key": "Access-Control-Allow-Origin", "value": "*" }
]
}
]
}
环境变量
// Use environment variables for sensitive data
const apiKey = process.env.API_KEY;
const publicUrl = process.env.NEXT_PUBLIC_APP_URL;
// Validate required env vars
if (!apiKey) {
throw new Error('API_KEY environment variable is required');
}
部署
预览部署
- 每个拉取请求都会获得一个预览部署
- 使用预览 URL 进行测试和审查
- 与相关方分享预览链接
生产环境部署
# Connect repo to Vercel (one-click from GitHub)
# Or use Vercel CLI
vercel --prod
Edge Config
import { get } from '@vercel/edge-config';
export async function getFeatureFlag(flag: string) {
const flags = await get('featureFlags');
return flags?.[flag] ?? false;
}
UI 和样式
Tailwind CSS 设置
// Use mobile-first responsive design
export function Card({ children }: { children: React.ReactNode }) {
return (
<div className="p-4 md:p-6 lg:p-8 rounded-lg bg-white shadow-sm">
{children}
</div>
);
}
Shadcn UI 组件
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog';
export function ConfirmDialog() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="destructive">Delete</Button>
</DialogTrigger>
<DialogContent>
<p>Are you sure?</p>
</DialogContent>
</Dialog>
);
}
无障碍访问
最佳实践
export function AccessibleButton() {
return (
<button
aria-label="Close dialog"
aria-expanded={isOpen}
aria-controls="dialog-content"
className="focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<XIcon aria-hidden="true" />
</button>
);
}
应避免的常见问题
- 不必要地使用 'use client'
- 未实现恰当的错误边界
- 忽视 Core Web Vitals 优化
- 未严格使用 TypeScript
- 将环境变量硬编码
- 未为异步组件添加 Suspense 边界
- 未优化图像
- 忽视无障碍访问要求
评论
支持 Markdown