Vercel部署实践
部署准备
项目优化
1. 构建优化
// next.config.js
module.exports = {
swcMinify: true,
images: {
domains: ['your-image-domain.com'],
},
experimental: {
optimizeCss: true,
optimizeImages: true,
},
}
2. 性能检查
# 运行性能检查
npm run build
npm run analyze # 需要安装@next/bundle-analyzer
# 检查构建输出
ls -lh .next/static/chunks/
3. 环境变量配置
# .env.production
NEXT_PUBLIC_API_URL=https://api.yourblog.com
NEXT_PUBLIC_SITE_URL=https://yourblog.com
DATABASE_URL=your_database_url
部署流程
初始部署
1. 连接仓库
- 使用UI
- 使用CLI
- 登录Vercel控制台
- 点击"New Project"
- 导入Git仓库
- 配置项目设置
# 安装Vercel CLI
npm i -g vercel
# 登录
vercel login
# 部署
vercel
2. 部署配置
// vercel.json
{
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@vercel/next"
}
],
"routes": [
{
"src": "/api/.*",
"headers": {
"Access-Control-Allow-Origin": "*"
}
}
]
}
自动部署配置
GitHub Actions配置
# .github/workflows/deploy.yml
name: Deploy to Vercel
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Dependencies
run: npm ci
- name: Run Tests
run: npm test
- name: Deploy to Vercel
if: success()
run: npx vercel --token ${VERCEL_TOKEN}
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
分支预览
// scripts/comment-on-pr.js
async function commentOnPR(deployment) {
const { GITHUB_TOKEN, PR_NUMBER } = process.env;
const comment = `
🚀 Preview deployment is ready!
URL: ${deployment.url}
Branch: ${deployment.git.branch}
Commit: ${deployment.git.commitSha}
`;
// 使用GitHub API发送评论
// ...
}
域名配置
DNS设置
添加域名
- 在Vercel项目设置中添加域名
- 配置DNS记录:
# A记录
Type: A
Name: @
Value: 76.76.21.21
# CNAME记录
Type: CNAME
Name: www
Value: cname.vercel-dns.com
SSL配置
{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "Strict-Transport-Security",
"value": "max-age=31536000; includeSubDomains"
}
]
}
]
}
监控和优化
性能监控
Web Vitals监控
// lib/analytics.ts
import { Analytics } from '@vercel/analytics';
export function reportWebVitals({ id, name, label, value }) {
Analytics.track(name, {
id,
label,
value,
page: window.location.pathname,
});
}
性能预算设置
// performance-budget.json
{
"resourceSizes": [
{
"resourceType": "script",
"budget": 300
},
{
"resourceType": "total",
"budget": 1000
}
],
"resourceCounts": [
{
"resourceType": "third-party",
"budget": 10
}
]
}
错误监控
Sentry集成
// lib/sentry.ts
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 1.0,
environment: process.env.NODE_ENV,
integrations: [
new Sentry.Integrations.BrowserTracing({
tracingOrigins: ['localhost', 'yourblog.com'],
}),
],
});
export function captureException(error: Error, context?: any) {
Sentry.withScope((scope) => {
if (context) {
scope.setExtras(context);
}
Sentry.captureException(error);
});
}
错误边界组件
// components/ErrorBoundary.tsx
import React from 'react';
import { captureException } from '@/lib/sentry';
interface Props {
children: React.ReactNode;
fallback?: React.ReactNode;
}
interface State {
hasError: boolean;
}
export class ErrorBoundary extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
captureException(error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div className="error-container">
<h2>Something went wrong</h2>
<button onClick={() => this.setState({ hasError: false })}>
Try again
</button>
</div>
);
}
return this.props.children;
}
}
缓存优化
CDN缓存配置
// pages/_middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const response = NextResponse.next();
// 静态资源缓存
if (request.nextUrl.pathname.startsWith('/_next/static/')) {
response.headers.set(
'Cache-Control',
'public, max-age=31536000, immutable'
);
}
// API响应缓存
if (request.nextUrl.pathname.startsWith('/api/')) {
response.headers.set(
'Cache-Control',
'public, s-maxage=60, stale-while-revalidate=300'
);
}
return response;
}
静态生成优化
// lib/cache.ts
import { redis } from './redis';
export async function getCachedData(key: string) {
const cached = await redis.get(key);
if (cached) {
return JSON.parse(cached);
}
return null;
}
export async function setCachedData(
key: string,
data: any,
ttl: number = 3600
) {
await redis.setex(key, ttl, JSON.stringify(data));
}