AWS Amplify Gen2 + Next.js App Router 开发指南
本文档提供了 AWS Amplify Gen2 与 Next.js App Router 集成的全面指南,帮助开发者构建现代化的全栈应用。
Amplify Gen2 与 Next.js App Router 概述
Amplify Gen2 与 Next.js App Router 的集成为开发者提供了构建现代、无服务器 Web 应用的强大能力,包括身份验证、数据管理、存储和实时功能。
- Amplify Gen2: AWS Amplify 的最新版本,通过 TypeScript 定义后端资源,基于 AWS CDK 构建,提供更灵活的云资源管理。
- Next.js App Router: Next.js 的新一代路由系统,支持 React Server Components、嵌套路由和更高级的应用功能。
版本兼容性
- Amplify JS v6 支持 Next.js 版本范围:
>=13.5.0 <16.0.0
- Node.js: v18.17 或更高版本
- npm: v9 或更高版本
参考资料:Amplify Documentation for Next.js
项目初始化和设置
1. 创建 Next.js 项目
# 创建新的 Next.js 项目(使用 App Router)
npx create-next-app@latest my-amplify-app --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd my-amplify-app
2. 初始化 Amplify
# 在项目根目录初始化 Amplify
npm create amplify@latest
# 或者在现有项目中
npm create amplify@latest -- --yes
3. 安装必要依赖
# 安装 Amplify 库
npm install aws-amplify @aws-amplify/adapter-nextjs
# 如果使用 AI 功能
npm install @aws-amplify/ui-react-ai
# 如果使用 UI 组件
npm install @aws-amplify/ui-react
后端配置
1. 基础后端结构
// amplify/backend.ts
import { defineBackend } from '@aws-amplify/backend';
import { auth } from './auth/resource';
import { data } from './data/resource';
import { storage } from './storage/resource';
export const backend = defineBackend({
auth,
data,
storage,
});
2. 认证配置
// amplify/auth/resource.ts
import { defineAuth } from '@aws-amplify/backend';
export const auth = defineAuth({
loginWith: {
email: true,
},
userAttributes: {
givenName: {
mutable: true,
required: true,
},
familyName: {
mutable: true,
required: true,
},
},
// 密码策略
passwordPolicy: {
minLength: 8,
requireLowercase: true,
requireUppercase: true,
requireNumbers: true,
requireSymbols: true,
},
});
3. 数据模型配置
// amplify/data/resource.ts
import { type ClientSchema, a, defineData } from '@aws-amplify/backend';
const schema = a.schema({
// 基础数据模型
Todo: a
.model({
content: a.string(),
done: a.boolean(),
priority: a.enum(['LOW', 'MEDIUM', 'HIGH']),
createdAt: a.datetime(),
updatedAt: a.datetime(),
})
.authorization((allow) => [allow.owner()]),
// AI 对话路由
chat: a.conversation({
aiModel: a.ai.model('Claude 3 Haiku'),
systemPrompt: `你是一个有用的AI助手。请用中文回答用户的问题。`,
})
.authorization((allow) => [allow.owner()]),
// AI 生成路由
generateSummary: a.generation({
aiModel: a.ai.model('Claude 3 Haiku'),
systemPrompt: '请为以下内容生成简洁的摘要:',
})
.authorization((allow) => [allow.authenticated()]),
});
export type Schema = ClientSchema<typeof schema>;
export const data = defineData({
schema,
authorizationModes: {
defaultAuthorizationMode: 'userPool',
// 支持多种授权模式
apiKeyAuthorizationMode: {
expiresInDays: 30,
},
},
});
4. 存储配置
// amplify/storage/resource.ts
import { defineStorage } from '@aws-amplify/backend';
export const storage = defineStorage({
name: 'myProjectFiles',
access: (allow) => ({
// 用户私有文件
'private/{entity_id}/*': [
allow.entity('identity').to(['read', 'write', 'delete'])
],
// 用户个人资料图片
'profile-pictures/{entity_id}/*': [
allow.entity('identity').to(['read', 'write', 'delete'])
],
// 公共文件
'public/*': [
allow.guest.to(['read']),
allow.authenticated.to(['read', 'write'])
],
})
});
Next.js 前端集成
1. 服务器端工具配置
// utils/amplifyServerUtils.ts
import { createServerRunner } from '@aws-amplify/adapter-nextjs';
import outputs from '../amplify_outputs.json';
export const { runWithAmplifyServerContext } = createServerRunner({
config: outputs,
});
2. 客户端配置
// app/layout.tsx
'use client';
import { Authenticator } from '@aws-amplify/ui-react';
import { Amplify } from 'aws-amplify';
import outputs from '../amplify_outputs.json';
import '@aws-amplify/ui-react/styles.css';
// 重要:在 Next.js 中必须设置 ssr: true
Amplify.configure(outputs, { ssr: true });
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="zh">
<body>
<Authenticator.Provider>
{children}
</Authenticator.Provider>
</body>
</html>
);
}
3. 认证组件
// components/AuthWrapper.tsx
'use client';
import { Authenticator } from '@aws-amplify/ui-react';
import { translations } from '@aws-amplify/ui-react';
import { I18n } from 'aws-amplify/utils';
// 设置中文翻译
I18n.putVocabularies(translations);
I18n.setLanguage('zh');
const authTranslations = {
zh: {
'Sign In': '登录',
'Sign Up': '注册',
'Enter your email': '请输入邮箱',
'Enter your password': '请输入密码',
// 更多翻译...
}
};
I18n.putVocabularies(authTranslations);
export function AuthWrapper({ children }: { children: React.ReactNode }) {
return (
<Authenticator>
{children}
</Authenticator>
);
}
4. 服务器组件中的数据获取
// app/todos/page.tsx
import { generateClient } from 'aws-amplify/api/server';
import { cookies } from 'next/headers';
import { runWithAmplifyServerContext } from '@/utils/amplifyServerUtils';
import { type Schema } from '@/amplify/data/resource';
export default async function TodosPage() {
const todos = await runWithAmplifyServerContext({
nextServerContext: { cookies },
operation: async (contextSpec) => {
const client = generateClient<Schema>(contextSpec);
try {
const response = await client.models.Todo.list();
return response.data;
} catch (error) {
console.error('获取待办事项失败:', error);
return [];
}
},
});
return (
<div className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">我的待办事项</h1>
<div className="space-y-2">
{todos.map((todo) => (
<div key={todo.id} className="p-3 border rounded-lg">
<p className={todo.done ? 'line-through text-gray-500' : ''}>
{todo.content}
</p>
<span className={`text-xs px-2 py-1 rounded ${
todo.priority === 'HIGH' ? 'bg-red-100 text-red-800' :
todo.priority === 'MEDIUM' ? 'bg-yellow-100 text-yellow-800' :
'bg-green-100 text-green-800'
}`}>
{todo.priority}
</span>
</div>
))}
</div>
</div>
);
}
5. 客户端组件中的数据操作
// components/TodoForm.tsx
'use client';
import { useState } from 'react';
import { generateClient } from 'aws-amplify/data';
import { type Schema } from '@/amplify/data/resource';
const client = generateClient<Schema>();
export function TodoForm() {
const [content, setContent] = useState('');
const [priority, setPriority] = useState<'LOW' | 'MEDIUM' | 'HIGH'>('MEDIUM');
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!content.trim()) return;
setIsLoading(true);
try {
await client.models.Todo.create({
content: content.trim(),
done: false,
priority,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
setContent('');
// 触发页面重新加载或使用状态管理
window.location.reload();
} catch (error) {
console.error('创建待办事项失败:', error);
} finally {
setIsLoading(false);
}
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<input
type="text"
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="输入待办事项..."
className="w-full p-2 border rounded-lg"
disabled={isLoading}
/>
</div>
<div>
<select
value={priority}
onChange={(e) => setPriority(e.target.value as any)}
className="p-2 border rounded-lg"
disabled={isLoading}
>
<option value="LOW">低优先级</option>
<option value="MEDIUM">中优先级</option>
<option value="HIGH">高优先级</option>
</select>
</div>
<button
type="submit"
disabled={isLoading || !content.trim()}
className="px-4 py-2 bg-blue-500 text-white rounded-lg disabled:opacity-50"
>
{isLoading ? '添加中...' : '添加待办事项'}
</button>
</form>
);
}
AI 功能集成
1. AI 对话组件
// components/ChatBot.tsx
'use client';
import { useState } from 'react';
import { generateClient } from 'aws-amplify/data';
import { type Schema } from '@/amplify/data/resource';
const client = generateClient<Schema>();
export function ChatBot() {
const [messages, setMessages] = useState<Array<{
role: 'user' | 'assistant';
content: string;
}>>([]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const sendMessage = async () => {
if (!input.trim()) return;
const userMessage = { role: 'user' as const, content: input };
setMessages(prev => [...prev, userMessage]);
setInput('');
setIsLoading(true);
try {
// 创建新对话或使用现有对话
const conversation = await client.conversations.chat.create();
// 发送消息并获取流式响应
const response = await conversation.sendMessage({
content: [{ text: input }]
});
// 处理流式响应
let assistantMessage = '';
for await (const chunk of response) {
if (chunk.contentBlockDelta?.delta?.text) {
assistantMessage += chunk.contentBlockDelta.delta.text;
// 实时更新UI
setMessages(prev => {
const newMessages = [...prev];
const lastMessage = newMessages[newMessages.length - 1];
if (lastMessage?.role === 'assistant') {
lastMessage.content = assistantMessage;
} else {
newMessages.push({ role: 'assistant', content: assistantMessage });
}
return newMessages;
});
}
}
} catch (error) {
console.error('发送消息失败:', error);
setMessages(prev => [...prev, {
role: 'assistant',
content: '抱歉,发生了错误。请稍后再试。'
}]);
} finally {
setIsLoading(false);
}
};
return (
<div className="max-w-2xl mx-auto p-4">
<div className="h-96 overflow-y-auto border rounded-lg p-4 mb-4 space-y-4">
{messages.map((message, index) => (
<div
key={index}
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className={`max-w-xs lg:max-w-md px-4 py-2 rounded-lg ${
message.role === 'user'
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-800'
}`}
>
{message.content}
</div>
</div>
))}
{isLoading && (
<div className="flex justify-start">
<div className="bg-gray-200 text-gray-800 px-4 py-2 rounded-lg">
正在思考...
</div>
</div>
)}
</div>
<div className="flex space-x-2">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && !isLoading && sendMessage()}
placeholder="输入您的消息..."
className="flex-1 p-2 border rounded-lg"
disabled={isLoading}
/>
<button
onClick={sendMessage}
disabled={isLoading || !input.trim()}
className="px-4 py-2 bg-blue-500 text-white rounded-lg disabled:opacity-50"
>
发送
</button>
</div>
</div>
);
}
沙盒环境管理
1. 开发环境设置
# 启动沙盒(监听模式)
npx ampx sandbox --identifier dev-yourname
# 启动前端开发服务器
npm run dev
2. 多环境管理
# 开发环境
npx ampx sandbox --identifier dev-feature-x --once
# 测试环境
npx ampx sandbox --identifier test-shared --once
# 切换环境(更新 amplify_outputs.json)
npx ampx sandbox --identifier target-env --once --outputs-out-dir .
3. 环境状态检查
// utils/environmentUtils.ts
import outputs from '../amplify_outputs.json';
export function getCurrentEnvironment() {
const userPoolId = outputs.auth.user_pool_id;
// 根据 User Pool ID 识别环境
if (userPoolId.includes('specific-id')) {
return 'production';
} else if (userPoolId.includes('test-id')) {
return 'testing';
} else {
return 'development';
}
}
export function EnvironmentBadge() {
const env = getCurrentEnvironment();
if (process.env.NODE_ENV === 'development') {
return (
<div className="fixed top-4 right-4 bg-blue-500 text-white px-2 py-1 rounded text-xs">
ENV: {env}
</div>
);
}
return null;
}
部署和托管
1. 生产部署
# 部署到生产分支
npx ampx deploy --branch main
# 部署到特定环境
npx ampx deploy --branch staging
2. 构建配置
# amplify.yml
version: 1
frontend:
phases:
preBuild:
commands:
- nvm install 18
- nvm use 18
- npm ci
build:
commands:
- npm run build
artifacts:
baseDirectory: .next
files:
- '**/*'
cache:
paths:
- node_modules/**/*
- .next/cache/**/*
最佳实践
1. 项目结构
src/
├── app/ # Next.js App Router
│ ├── (auth)/ # 认证相关页面
│ ├── dashboard/ # 仪表板
│ ├── todos/ # 待办事项
│ └── chat/ # AI 聊天
├── components/ # 可复用组件
│ ├── ui/ # 基础 UI 组件
│ ├── auth/ # 认证组件
│ └── features/ # 功能组件
├── hooks/ # 自定义 Hooks
├── utils/ # 工具函数
└── types/ # TypeScript 类型定义
2. 错误处理
// utils/errorHandler.ts
export function handleAmplifyError(error: any) {
console.error('Amplify 错误:', error);
if (error.errors) {
// GraphQL 错误
return error.errors.map((e: any) => e.message).join(', ');
} else if (error.message) {
// 一般错误
return error.message;
} else {
return '发生未知错误';
}
}
3. 类型安全
// types/amplify.ts
import { type Schema } from '@/amplify/data/resource';
export type Todo = Schema['Todo']['type'];
export type CreateTodoInput = Schema['Todo']['createType'];
export type UpdateTodoInput = Schema['Todo']['updateType'];
故障排除
1. 常见问题
Library Not Configured 错误:
// 确保在 layout.tsx 中正确配置
Amplify.configure(outputs, { ssr: true });
沙盒连接问题:
# 检查当前连接的沙盒
grep "user_pool_id" amplify_outputs.json
# 切换到正确的沙盒
npx ampx sandbox --identifier correct-sandbox --once --outputs-out-dir .
2. 调试技巧
// 开发模式下显示调试信息
if (process.env.NODE_ENV === 'development') {
console.log('Amplify 配置:', outputs);
console.log('当前用户池:', outputs.auth.user_pool_id);
console.log('GraphQL 端点:', outputs.data.url);
}