AWS Amplify Gen2 + Next.js App Router 开发指南

本文档提供了 AWS Amplify Gen2 与 Next.js App Router 集成的全面指南,帮助开发者构建现代化的全栈应用。

Amplify Gen2 与 Next.js App Router 概述

Amplify Gen2Next.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);
}

参考资料:

‹ Next Post Previous Post ›
No Comment
Add Comment
comment url
⬆️