{post.title}
{post.content}
搞过的人都知道,Next.js项目写出来容易,但要跑得稳、扩展性好、SEO漂亮,那才是真本事。新手只知道npm run dev跑起来就完事,结果上了生产环境一堆问题。本文解决的是:如何从零搭建一个生产级别的Next.js项目骨架,包含目录规范、性能优化、环境隔离、SEO配置这些实战中必踩的坑。
# 创建项目,推荐App Router(Next.js 13+)
npx create-next-app@latest my-app --typescript --eslint --app --src-dir --import-alias "@/*"
# 进入目录
cd my-app
# 查看项目结构
ls -lamy-app/
├── src/
│ ├── app/
│ │ ├── globals.css
│ │ ├── layout.tsx
│ │ └── page.tsx
│ └── components/
├── public/
├── next.config.js
├── package.json
├── tsconfig.json
└── .env.local# tsconfig.json 已自动配置,但确认一下
cat tsconfig.json{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true, // 必须开启strict
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }],
"paths": {
"@/*": ["./src/*"] // 路径别名生效
}
},
"include": ["next-env.d.ts", "**/*.ts", "**.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}# 创建环境变量文件
touch .env
touch .env.local
touch .env.production
# .env(通用默认值,提交到Git)
cat > .env << 'EOF'
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_SITE_NAME=MyApp
EOF
# .env.local(本地敏感值,不提交)
cat > .env.local << 'EOF'
# 数据库连接
DATABASE_URL=postgresql://user:YOUR_PASSWORD@localhost:5432/mydb
# API密钥
STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxxxx
EOF
# .gitignore 添加
echo -e "\n# Environment\n.env.local" >> .gitignore.env.local 内容:
DATABASE_URL=postgresql://user:YOUR_PASSWORD@localhost:5432/mydb
STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxxxx
.env.local 已被 .gitignore 忽略 ✓
环境变量加载优先级:.env.local > .env.production > .env# 在 next.config.js 中配置图片域名白名单
cat > next.config.js << 'EOF'
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: '**.example.com',
},
{
protocol: 'https',
hostname: 'images.unsplash.com',
},
],
formats: ['image/avif', 'image/webp'], // 优先AVIF
},
}
module.exports = nextConfig
EOF
# 重启开发服务器使配置生效
pkill -f "next dev" && npm run dev &⚠️ Warning: The `images.remotePatterns` configuration is now set.
Using the unoptimized image API directly is not allowed.
Image optimization enabled:
- Formats: avif, webp
- Remote domains: example.com, images.unsplash.com
⚠️ ⚠️ WARNING: Skipping type check...
Ready - started server on http://localhost:3000
Environment variables loaded from: .env.local# 创建动态路由页面(支持SSG+ISR)
mkdir -p src/app/posts/[slug]
cat > src/app/posts/[slug]/page.tsx << 'EOF'
import { notFound } from 'next/navigation'
import { Metadata } from 'next'
// 静态生成参数(构建时确定)
export async function generateStaticParams() {
const res = await fetch('https://api.example.com/posts')
const posts = await res.json()
return posts.map((post) => ({ slug: post.slug }))
}
// 动态元数据
export async function generateMetadata({ params }): Promise {
const { slug } = params
const res = await fetch(`https://api.example.com/posts/${slug}`)
const post = await res.json()
return {
title: post.title,
description: post.excerpt,
openGraph: {
images: [post.coverImage],
},
}
}
// 页面组件
async function getPost(slug: string) {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
next: { revalidate: 3600 }, // ISR,每小时重新验证
})
if (!res.ok) return null
return res.json()
}
export default async function PostPage({ params }) {
const post = await getPost(params.slug)
if (!post) {
notFound()
}
return (
{post.title}
{post.content}
)
}
EOF
# 验证类型
npm run build 2>&1 | head -30 ✓ Compiled successfully
✓ Collecting page data...
生成静态参数: /posts/nextjs-best-practices
生成静态参数: /posts/react-server-components
生成静态参数: /posts/typescript-tips
✓ 生成 3 个页面 ( ISR: revalidate=3600 )
Route (App) Size First Load JS
┌ ● /posts/[slug] 1.42 kB 86.3 kB
├ /posts/nextjs-best-practices
├ /posts/react-server-components
└ /posts/typescript-tips
# 创建大型组件(假设有个图表库很大)
mkdir -p src/components/Charts
cat > src/components/Charts/HeavyChart.tsx << 'EOF'
'use client'
// HeavyChart.tsx - 假设这个组件很重
export function HeavyChart() {
return 这是一个很重的图表组件
}
EOF
# 在页面中使用动态导入
cat > src/app/dashboard/page.tsx << 'EOF'
import dynamic from 'next/dynamic'
// 动态导入,不打包到首屏
const HeavyChart = dynamic(
() => import('@/components/Charts/HeavyChart').then(mod => mod.HeavyChart),
{
loading: () => 加载图表中...
,
ssr: false // 客户端渲染
}
)
export default function Dashboard() {
return (
仪表盘
)
}
EOF
# 检查bundle大小
npm run build 2>&1 | grep -A 20 "Route (App)"Route (App) Size First Load JS
┌ ● /dashboard 412 B 68.2 kB ← 首屏JS大幅减少
│ └ ○ HeavyChart 0 B 245.6 kB ← 懒加载,按需加载
# 创建SEO工具函数
mkdir -p src/lib
cat > src/lib/seo.ts << 'EOF'
import { Metadata } from 'next'
type SEOConfig = {
title: string
description: string
path: string
image?: string
}
export function getSEOConfig({ title, description, path, image }: SEOConfig): Metadata {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com'
const fullUrl = `${baseUrl}${path}`
const defaultImage = `${baseUrl}/og-default.png`
return {
title: `${title} | ${process.env.NEXT_PUBLIC_SITE_NAME}`,
description,
alternates: {
canonical: fullUrl,
},
openGraph: {
url: fullUrl,
title,
description,
images: [
{
url: image || defaultImage,
width: 1200,
height: 630,
},
],
type: 'website',
},
twitter: {
card: 'summary_large_image',
title,
description,
images: [image || defaultImage],
},
}
}
EOF
# 使用示例
cat > src/app/about/page.tsx << 'EOF'
import { getSEOConfig } from '@/lib/seo'
export const metadata = getSEOConfig({
title: '关于我们',
description: '了解我们的团队和使命',
path: '/about',
image: 'https://example.com/about-og.jpg'
})
export default function AboutPage() {
return 关于我们页面
}
EOF✓ Type checking passed
检查生成的head标签:
# 生产构建前全面检查
npm run build
# 启动生产服务器测试(生产环境必须用)
NODE_ENV=production node .next/standalone/server.js
# 验证HTTP头(安全头配置)
curl -I https://your-app.vercel.app 2>/dev/null | grep -E "(X-Frame-Options|Content-Security-Policy|X-XSS-Protection)"构建成功 ✓
生产服务器启动:
Server running on http://localhost:3000
HTTP安全响应头:
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline';
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin老手回答:SSR本身不慢,慢的是你fetch数据的方式。新手喜欢在服务端组件里直接fetch,不做缓存,不做并行,结果每次请求都要等上游API。解决方案:给fetch加cache: 'force-cache'或revalidate时间,用Promise.all并行fetch多个数据源。还有,别在服务端组件里用useEffect,那玩意儿只在客户端跑。
老手回答:检查三件事:第一,图片域名有没有加到next.config.js的remotePatterns里;第二,本地图片路径对不对,相对路径要用相对路径字符串;第三,生产环境CDN缓存问题。如果用Unsplash等外部图床,直接在remotePatterns里配通配符域名images.unsplash.com就行。
老手回答:先搞清楚Next.js环境变量规则:NEXT_PUBLIC_前缀的才会打包到客户端,其他都是服务端的。生产环境变量不要放.env文件,放云平台(Vercel/Railway/Render)的环境变量配置里,本地开发用.env.local。如果CI/CD里用docker,给docker-compose.yml里单独写env_file,别指望.env能自动生效。
老手回答:选App Router,没商量。Pages Router已经进入维护模式,Next.js团队所有新特性都只加App Router。SSR/SSG/CSR混用、React Server Components、流式渲染、React Suspense这些牛逼特性全是App Router专属。但如果你维护的是老项目在用Pages Router,别急着迁移,小项目还好,大项目迁移是血泪史,边用边学新技术就行。
Q5:npm run build通过了,生产环境还是白屏,怎么排查?老手回答:白屏问题90%是这几个原因:环境变量缺失(尤其是NEXT_PUBLIC_开头的)、API地址配置错误、TypeScript类型在运行时崩溃。排查步骤:第一步看浏览器console报错;第二步看服务端日志;第三步在页面组件外层包裹try-catch看是不是服务端抛异常;第四步检查next.config.js的basePath配置,如果应用不在根目录,basePath必须设置对。生产环境用standalone模式时,注意输出目录有没有包含所有依赖。
核心要点:
延伸阅读:
记住,最好的架构是能让接手的人不用问你就知道文件在哪、功能怎么改,而不是靠文档和口口相传。代码规范是团队的契约,写的时候偷懒,维护的时候还债。
上一篇: Monitoring:监控自动化告警
已经是最后一篇啦!