AI地牢游戏
模块 1: Monorepo 初始化
我们将从创建一个新的 monorepo 开始。在您选择的目录中运行以下命令:
npx create-nx-workspace@~21.0.3 dungeon-adventure --pm=pnpm --preset=@aws/nx-plugin --ci=skip
npx create-nx-workspace@~21.0.3 dungeon-adventure --pm=yarn --preset=@aws/nx-plugin --ci=skip
npx create-nx-workspace@~21.0.3 dungeon-adventure --pm=npm --preset=@aws/nx-plugin --ci=skip
npx create-nx-workspace@~21.0.3 dungeon-adventure --pm=bun --preset=@aws/nx-plugin --ci=skip
这将在 dungeon-adventure
目录中设置一个 NX monorepo,您可以在 vscode 中打开它。其结构应如下所示:
文件夹.nx/
- …
文件夹.vscode/
- …
文件夹node_modules/
- …
文件夹packages/ 你的子项目将存放在此处
- …
- .gitignore
- .npmrc
- .prettierignore
- .prettierrc
- nx.json 配置 Nx CLI 和 monorepo 默认设置
- package.json 定义所有 node 依赖项
- pnpm-lock.yaml 或 bun.lock, yarn.lock, package-lock.json(取决于包管理器)
- pnpm-workspace.yaml(如果使用 pnpm)
- README.md
- tsconfig.base.json 所有基于 node 的子项目都继承此配置
- tsconfig.json
现在我们可以使用 @aws/nx-plugin
开始创建不同的子项目了。
游戏 API
首先创建我们的游戏 API。为此,我们通过以下步骤创建一个名为 GameApi
的 tRPC API:
- 安装 Nx Console VSCode Plugin 如果您尚未安装
- 在VSCode中打开Nx控制台
- 点击
Generate (UI)
在"Common Nx Commands"部分 - 搜索
@aws/nx-plugin - ts#trpc-api
- 填写必需参数
- name: GameApi
- 点击
Generate
pnpm nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive
yarn nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive
npx nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive
bunx nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive
您还可以执行试运行以查看哪些文件会被更改
pnpm nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive --dry-run
yarn nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive --dry-run
npx nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive --dry-run
bunx nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive --dry-run
您应该会在文件树中看到一些新文件。
ts#trpc-api 更新文件
以下是 ts#trpc-api
生成器生成的所有文件列表。我们将重点检查文件树中突出显示的关键文件:
文件夹packages/
文件夹common/
文件夹constructs/
文件夹src/
文件夹app/ 应用特定的 CDK 构造
文件夹apis/
- game-api.ts 用于创建 tRPC API 的 CDK 构造
- index.ts
- …
- index.ts
文件夹core/ 通用 CDK 构造
文件夹api/
- rest-api.ts API Gateway Rest API 的基础 CDK 构造
- trpc-utils.ts trpc API CDK 构造的实用工具
- utils.ts API 构造的实用工具
- index.ts
- runtime-config.ts
- index.ts
- project.json
- …
文件夹types/ 共享类型
文件夹src/
- index.ts
- runtime-config.ts 被 CDK 和网站共同使用的接口定义
- project.json
- …
文件夹game-api/
文件夹backend/ tRPC 实现代码
文件夹src/
文件夹client/ 通常用于 TS 机器间调用的原生客户端
- index.ts
- sigv4.ts
文件夹middleware/ Powertools 工具链
- error.ts
- index.ts
- logger.ts
- metrics.ts
- tracer.ts
文件夹procedures/ API 过程/路由的具体实现
- echo.ts
- index.ts
- init.ts 设置上下文和中间件
- local-server.ts 本地运行 tRPC 服务器时使用
- router.ts 定义所有过程的 lambda 处理程序入口点
- project.json
- …
文件夹schema/
文件夹src/
文件夹procedures/
- echo.ts
- index.ts
- project.json
- …
- eslint.config.mjs
- vitest.workspace.ts
查看几个关键文件:
import { awsLambdaRequestHandler, CreateAWSLambdaContextOptions,} from '@trpc/server/adapters/aws-lambda';import { echo } from './procedures/echo.js';import { t } from './init.js';import { APIGatewayProxyEvent } from 'aws-lambda';
export const router = t.router;
export const appRouter = router({ echo,});
export const handler = awsLambdaRequestHandler({ router: appRouter, createContext: ( ctx: CreateAWSLambdaContextOptions<APIGatewayProxyEvent>, ) => ctx, responseMeta: () => ({ headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': '*', }, }),});
export type AppRouter = typeof appRouter;
路由文件定义了 tRPC API 的入口点,您需要在此声明所有 API 方法。如上所示,我们有一个名为 echo
的方法,其实现位于 ./procedures/echo.ts
文件中。
import { publicProcedure } from '../init.js';import { EchoInputSchema, EchoOutputSchema,} from ':dungeon-adventure/game-api-schema';
export const echo = publicProcedure .input(EchoInputSchema) .output(EchoOutputSchema) .query((opts) => ({ result: opts.input.message }));
该文件是 echo
方法的实现,通过声明输入和输出数据结构进行强类型约束。这些类型定义从 :dungeon-adventure/game-api-schema
项目导入,这是 schema 项目的路径别名。
import { z } from 'zod';
export const EchoInputSchema = z.object({ message: z.string(),});
export type IEchoInput = z.TypeOf<typeof EchoInputSchema>;
export const EchoOutputSchema = z.object({ result: z.string(),});
export type IEchoOutput = z.TypeOf<typeof EchoOutputSchema>;
所有 tRPC 模式定义都使用 Zod 定义,并通过 z.TypeOf
语法导出为 TypeScript 类型。
import { Construct } from 'constructs';import * as url from 'url';import { Code, Runtime, Function, FunctionProps, Tracing,} from 'aws-cdk-lib/aws-lambda';import { AuthorizationType, Cors, LambdaIntegration,} from 'aws-cdk-lib/aws-apigateway';import { Duration, Stack } from 'aws-cdk-lib';import { PolicyDocument, PolicyStatement, Effect, AccountPrincipal, AnyPrincipal,} from 'aws-cdk-lib/aws-iam';import { IntegrationBuilder, RestApiIntegration,} from '../../core/api/utils.js';import { RestApi } from '../../core/api/rest-api.js';import { Procedures, routerToOperations } from '../../core/api/trpc-utils.js';import { AppRouter, appRouter } from ':dungeon-adventure/game-api';
// 所有 API 操作名称的字符串联合类型type Operations = Procedures<AppRouter>;
/** * 创建 GameApi 构造的属性 * * @template TIntegrations - 操作名称到其集成的映射 */export interface GameApiProps< TIntegrations extends Record<Operations, RestApiIntegration>,> { /** * 操作名称到 API Gateway 集成的映射 */ integrations: TIntegrations;}
/** * 专门为 GameApi 创建和配置 AWS API Gateway REST API 的 CDK 构造 * @template TIntegrations - 操作名称到其集成的映射 */export class GameApi< TIntegrations extends Record<Operations, RestApiIntegration>,> extends RestApi<Operations, TIntegrations> { /** * 为所有操作创建默认集成,将每个操作实现为独立的 lambda 函数 * * @param scope - CDK 构造作用域 * @returns 包含默认 lambda 集成的 IntegrationBuilder */ public static defaultIntegrations = (scope: Construct) => { return IntegrationBuilder.rest({ operations: routerToOperations(appRouter), defaultIntegrationOptions: { runtime: Runtime.NODEJS_LATEST, handler: 'index.handler', code: Code.fromAsset( url.fileURLToPath( new URL( '../../../../../../dist/packages/game-api/backend/bundle', import.meta.url, ), ), ), timeout: Duration.seconds(30), tracing: Tracing.ACTIVE, environment: { AWS_CONNECTION_REUSE_ENABLED: '1', }, } satisfies FunctionProps, buildDefaultIntegration: (op, props: FunctionProps) => { const handler = new Function(scope, `GameApi${op}Handler`, props); return { handler, integration: new LambdaIntegration(handler) }; }, }); };
constructor( scope: Construct, id: string, props: GameApiProps<TIntegrations>, ) { super(scope, id, { apiName: 'GameApi', defaultMethodOptions: { authorizationType: AuthorizationType.IAM, }, defaultCorsPreflightOptions: { allowOrigins: Cors.ALL_ORIGINS, allowMethods: Cors.ALL_METHODS, }, policy: new PolicyDocument({ statements: [ // 允许部署账户中的任何 AWS 凭证调用 API // 如果需要,可以在此定义更细粒度的机器间访问控制 new PolicyStatement({ effect: Effect.ALLOW, principals: [new AccountPrincipal(Stack.of(scope).account)], actions: ['execute-api:Invoke'], resources: ['execute-api:/*'], }), // 开放 OPTIONS 方法以允许浏览器进行未经身份验证的预检请求 new PolicyStatement({ effect: Effect.ALLOW, principals: [new AnyPrincipal()], actions: ['execute-api:Invoke'], resources: ['execute-api:/*/OPTIONS/*'], }), ], }), operations: routerToOperations(appRouter), ...props, }); }}
这是定义 GameApi 的 CDK 构造。它提供了 defaultIntegrations
方法,自动为 tRPC API 中的每个过程创建 lambda 函数,指向已打包的 API 实现。这意味着在 cdk synth
时不会进行打包(与使用 NodeJsFunction 不同),因为打包已作为后端项目构建目标的一部分完成。
故事 API
现在创建我们的故事 API。为此,我们通过以下步骤创建一个名为 StoryApi
的 Fast API:
- 安装 Nx Console VSCode Plugin 如果您尚未安装
- 在VSCode中打开Nx控制台
- 点击
Generate (UI)
在"Common Nx Commands"部分 - 搜索
@aws/nx-plugin - py#fast-api
- 填写必需参数
- name: StoryApi
- 点击
Generate
pnpm nx g @aws/nx-plugin:py#fast-api --name=StoryApi --no-interactive
yarn nx g @aws/nx-plugin:py#fast-api --name=StoryApi --no-interactive
npx nx g @aws/nx-plugin:py#fast-api --name=StoryApi --no-interactive
bunx nx g @aws/nx-plugin:py#fast-api --name=StoryApi --no-interactive
您还可以执行试运行以查看哪些文件会被更改
pnpm nx g @aws/nx-plugin:py#fast-api --name=StoryApi --no-interactive --dry-run
yarn nx g @aws/nx-plugin:py#fast-api --name=StoryApi --no-interactive --dry-run
npx nx g @aws/nx-plugin:py#fast-api --name=StoryApi --no-interactive --dry-run
bunx nx g @aws/nx-plugin:py#fast-api --name=StoryApi --no-interactive --dry-run
您应该会在文件树中看到一些新文件。
py#fast-api 更新文件
以下是 py#fast-api
生成器生成的所有文件列表。我们将重点检查文件树中突出显示的关键文件:
文件夹.venv/ monorepo 的单一虚拟环境
- …
文件夹packages/
文件夹common/
文件夹constructs/
文件夹src/
文件夹app/ 应用特定的 CDK 构造
文件夹apis/
- story-api.ts 创建 Fast API 的 CDK 构造
- index.ts 更新以导出新的 story-api
- project.json 更新以添加对 story_api 的构建依赖
文件夹types/ 共享类型
文件夹src/
- runtime-config.ts 更新以添加 StoryApi
文件夹story_api/
文件夹story_api/ Python 模块
- init.py 设置 Powertools、FastAPI 和中间件
- main.py 包含所有路由的 lambda 入口点
文件夹tests/
- …
- .python-version
- project.json
- pyproject.toml
- project.json
- .python-version 固定的 uv Python 版本
- pyproject.toml
- uv.lock
import { Construct } from 'constructs';import * as url from 'url';import { Code, Runtime, Function, FunctionProps, Tracing,} from 'aws-cdk-lib/aws-lambda';import { AuthorizationType, Cors, LambdaIntegration,} from 'aws-cdk-lib/aws-apigateway';import { Duration, Stack } from 'aws-cdk-lib';import { PolicyDocument, PolicyStatement, Effect, AccountPrincipal, AnyPrincipal,} from 'aws-cdk-lib/aws-iam';import { IntegrationBuilder, RestApiIntegration,} from '../../core/api/utils.js';import { RestApi } from '../../core/api/rest-api.js';import { OPERATION_DETAILS, Operations,} from '../../generated/story-api/metadata.gen.js';
/** * 创建 StoryApi 构造的属性 * * @template TIntegrations - 操作名称到其集成的映射 */export interface StoryApiProps< TIntegrations extends Record<Operations, RestApiIntegration>,> { /** * 操作名称到 API Gateway 集成的映射 */ integrations: TIntegrations;}
/** * 专门为 StoryApi 创建和配置 AWS API Gateway REST API 的 CDK 构造 * @template TIntegrations - 操作名称到其集成的映射 */export class StoryApi< TIntegrations extends Record<Operations, RestApiIntegration>,> extends RestApi<Operations, TIntegrations> { /** * 为所有操作创建默认集成,将每个操作实现为独立的 lambda 函数 * * @param scope - CDK 构造作用域 * @returns 包含默认 lambda 集成的 IntegrationBuilder */ public static defaultIntegrations = (scope: Construct) => { return IntegrationBuilder.rest({ operations: OPERATION_DETAILS, defaultIntegrationOptions: { runtime: Runtime.PYTHON_3_12, handler: 'story_api.main.handler', code: Code.fromAsset( url.fileURLToPath( new URL( '../../../../../../dist/packages/story_api/bundle', import.meta.url, ), ), ), timeout: Duration.seconds(30), tracing: Tracing.ACTIVE, environment: { AWS_CONNECTION_REUSE_ENABLED: '1', }, } satisfies FunctionProps, buildDefaultIntegration: (op, props: FunctionProps) => { const handler = new Function(scope, `StoryApi${op}Handler`, props); return { handler, integration: new LambdaIntegration(handler) }; }, }); };
constructor( scope: Construct, id: string, props: StoryApiProps<TIntegrations>, ) { super(scope, id, { apiName: 'StoryApi', defaultMethodOptions: { authorizationType: AuthorizationType.IAM, }, defaultCorsPreflightOptions: { allowOrigins: Cors.ALL_ORIGINS, allowMethods: Cors.ALL_METHODS, }, policy: new PolicyDocument({ statements: [ // 允许部署账户中的任何 AWS 凭证调用 API // 如果需要,可以在此定义更细粒度的机器间访问控制 new PolicyStatement({ effect: Effect.ALLOW, principals: [new AccountPrincipal(Stack.of(scope).account)], actions: ['execute-api:Invoke'], resources: ['execute-api:/*'], }), // 开放 OPTIONS 方法以允许浏览器进行未经身份验证的预检请求 new PolicyStatement({ effect: Effect.ALLOW, principals: [new AnyPrincipal()], actions: ['execute-api:Invoke'], resources: ['execute-api:/*/OPTIONS/*'], }), ], }), operations: OPERATION_DETAILS, ...props, }); }}
这是定义 StoryApi 的 CDK 构造。它提供了 defaultIntegrations
方法,自动为 FastAPI 中的每个操作创建 lambda 函数,指向已打包的 API 实现。这意味着在 cdk synth
时不会进行打包(与使用 PythonFunction 不同),因为打包已作为后端项目构建目标的一部分完成。
export type ApiUrl = string;// eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-empty-interfaceexport interface IRuntimeConfig { apis: { GameApi: ApiUrl; StoryApi: ApiUrl; };}
这是生成器执行 AST 转换的示例,保留所有现有代码并进行更新。您可以看到 StoryApi
被添加到 IRuntimeConfig
定义中,这意味着当最终被前端使用时,将强制执行类型安全!
from .init import app, lambda_handler, tracer
handler = lambda_handler
@app.get("/")@tracer.capture_methoddef read_root(): return {"Hello": "World"}
这是定义所有 API 方法的地方。如上所示,我们有一个映射到 GET /
路由的 read_root
方法。您可以使用 Pydantic 声明方法输入和输出以确保类型安全。
游戏 UI:网站
现在创建用于与游戏交互的 UI。为此,我们通过以下步骤创建一个名为 GameUI
的网站:
- 安装 Nx Console VSCode Plugin 如果您尚未安装
- 在VSCode中打开Nx控制台
- 点击
Generate (UI)
在"Common Nx Commands"部分 - 搜索
@aws/nx-plugin - ts#cloudscape-website
- 填写必需参数
- name: GameUI
- 点击
Generate
pnpm nx g @aws/nx-plugin:ts#cloudscape-website --name=GameUI --no-interactive
yarn nx g @aws/nx-plugin:ts#cloudscape-website --name=GameUI --no-interactive
npx nx g @aws/nx-plugin:ts#cloudscape-website --name=GameUI --no-interactive
bunx nx g @aws/nx-plugin:ts#cloudscape-website --name=GameUI --no-interactive
您还可以执行试运行以查看哪些文件会被更改
pnpm nx g @aws/nx-plugin:ts#cloudscape-website --name=GameUI --no-interactive --dry-run
yarn nx g @aws/nx-plugin:ts#cloudscape-website --name=GameUI --no-interactive --dry-run
npx nx g @aws/nx-plugin:ts#cloudscape-website --name=GameUI --no-interactive --dry-run
bunx nx g @aws/nx-plugin:ts#cloudscape-website --name=GameUI --no-interactive --dry-run
您应该会在文件树中看到一些新文件。
ts#cloudscape-website 更新文件
以下是 ts#cloudscape-website
生成器生成的所有文件列表。我们将重点检查文件树中突出显示的关键文件:
文件夹packages/
文件夹common/
文件夹constructs/
文件夹src/
文件夹app/ 应用特定的 CDK 构造
文件夹static-websites/
- game-ui.ts 创建游戏 UI 的 CDK 构造
文件夹core/
- static-website.ts 通用静态网站构造
文件夹game-ui/
文件夹public/
- …
文件夹src/
文件夹components/
文件夹AppLayout/
- index.ts 整体页面布局:页头、页脚、侧边栏等
- navitems.ts 侧边栏导航项
文件夹hooks/
- useAppLayout.tsx 允许动态设置通知、页面样式等
文件夹routes/ @tanstack/react-router 基于文件的路由
- index.tsx 根 ’/’ 页面重定向到 ‘/welcome’
- __root.tsx 所有页面使用此组件作为基础
文件夹welcome/
- index.tsx
- config.ts
- main.tsx React 入口点
- routeTree.gen.ts 由 @tanstack/react-router 自动更新
- styles.css
- index.html
- project.json
- vite.config.ts
- …
import * as url from 'url';import { Construct } from 'constructs';import { StaticWebsite } from '../../core/index.js';
export class GameUI extends StaticWebsite { constructor(scope: Construct, id: string) { super(scope, id, { websiteFilePath: url.fileURLToPath( new URL( '../../../../../../dist/packages/game-ui/bundle', import.meta.url, ), ), }); }}
这是定义 GameUI 的 CDK 构造。它已配置了指向 Vite 构建输出的文件路径,这意味着在 build
时,打包发生在 game-ui 项目的构建目标中,其输出在此处使用。
import React from 'react';import { createRoot } from 'react-dom/client';import { I18nProvider } from '@cloudscape-design/components/i18n';import messages from '@cloudscape-design/components/i18n/messages/all.en';import { RouterProvider, createRouter } from '@tanstack/react-router';import { routeTree } from './routeTree.gen';
import '@cloudscape-design/global-styles/index.css';
const router = createRouter({ routeTree });
// 为类型安全注册路由实例declare module '@tanstack/react-router' { interface Register { router: typeof router; }}
const root = document.getElementById('root');root && createRoot(root).render( <React.StrictMode> <I18nProvider locale="en" messages={[messages]}> <RouterProvider router={router} /> </I18nProvider> </React.StrictMode>, );
这是挂载 React 的入口点。如所示,它最初仅配置了基于文件路由的 @tanstack/react-router
。这意味着只要开发服务器在运行,您只需在 routes
文件夹中创建文件,@tanstack/react-router
就会自动生成样板文件并更新 routeTree.gen.ts
文件。该文件以类型安全的方式维护所有路由,因此当使用 <Link>
时,to
选项仅显示有效路由。更多信息请参考 @tanstack/react-router
文档。
import { ContentLayout, Header, SpaceBetween, Container,} from '@cloudscape-design/components';import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/welcome/')({ component: RouteComponent,});
function RouteComponent() { return ( <ContentLayout header={<Header>Welcome</Header>}> <SpaceBetween size="l"> <Container>欢迎来到您的新 Cloudscape 网站!</Container> </SpaceBetween> </ContentLayout> );}
当导航到 /welcome
路由时将渲染此组件。@tanstack/react-router
将在您创建/移动此文件时管理 Route
(只要开发服务器在运行)。这将在本教程的后续部分展示。
游戏 UI:认证
现在通过以下步骤配置我们的游戏 UI,要求通过 Amazon Cognito 进行认证访问:
- 安装 Nx Console VSCode Plugin 如果您尚未安装
- 在VSCode中打开Nx控制台
- 点击
Generate (UI)
在"Common Nx Commands"部分 - 搜索
@aws/nx-plugin - ts#cloudscape-website#auth
- 填写必需参数
- cognitoDomain: game-ui
- project: @dungeon-adventure/game-ui
- allowSignup: true
- 点击
Generate
pnpm nx g @aws/nx-plugin:ts#cloudscape-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive
yarn nx g @aws/nx-plugin:ts#cloudscape-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive
npx nx g @aws/nx-plugin:ts#cloudscape-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive
bunx nx g @aws/nx-plugin:ts#cloudscape-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive
您还可以执行试运行以查看哪些文件会被更改
pnpm nx g @aws/nx-plugin:ts#cloudscape-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive --dry-run
yarn nx g @aws/nx-plugin:ts#cloudscape-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive --dry-run
npx nx g @aws/nx-plugin:ts#cloudscape-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive --dry-run
bunx nx g @aws/nx-plugin:ts#cloudscape-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive --dry-run
您应该会在文件树中看到一些新增/变更的文件。
ts#cloudscape-website#auth 更新文件
以下是 ts#cloudscape-website#auth
生成器生成/更新的所有文件列表。我们将重点检查文件树中突出显示的关键文件:
文件夹packages/
文件夹common/
文件夹constructs/
文件夹src/
文件夹core/
- user-identity.ts 创建用户/身份池的 CDK 构造
文件夹types/
文件夹src/
- runtime-config.ts 更新以添加 cognitoProps
文件夹game-ui/
文件夹src/
文件夹components/
文件夹AppLayout/
- index.tsx 在页头添加已登录用户/注销功能
文件夹CognitoAuth/
- index.ts 管理 Cognito 登录
文件夹RuntimeConfig/
- index.tsx 获取
runtime-config.json
并通过上下文提供给子组件
- index.tsx 获取
文件夹hooks/
- useRuntimeConfig.tsx
- main.tsx 更新以添加 Cognito
import CognitoAuth from './components/CognitoAuth';import RuntimeConfigProvider from './components/RuntimeConfig';import React from 'react';import { createRoot } from 'react-dom/client';import { I18nProvider } from '@cloudscape-design/components/i18n';import messages from '@cloudscape-design/components/i18n/messages/all.en';import { RouterProvider, createRouter } from '@tanstack/react-router';import { routeTree } from './routeTree.gen';import '@cloudscape-design/global-styles/index.css';const router = createRouter({ routeTree });// 为类型安全注册路由实例declare module '@tanstack/react-router' { interface Register { router: typeof router; }}const root = document.getElementById('root');root && createRoot(root).render( <React.StrictMode> <I18nProvider locale="en" messages={[messages]}> <RuntimeConfigProvider> <CognitoAuth> <RouterProvider router={router} /> </CognitoAuth> </RuntimeConfigProvider> </I18nProvider> </React.StrictMode>, );
通过 AST 转换将 RuntimeConfigProvider
和 CognitoAuth
组件添加到 main.tsx
文件中。这使得 CognitoAuth
组件能够通过获取包含必要 Cognito 连接配置的 runtime-config.json
来与 Amazon Cognito 进行认证,从而正确调用后端服务。
游戏 UI:连接故事 API
现在配置我们的游戏 UI 连接到之前创建的故事 API:
- 安装 Nx Console VSCode Plugin 如果您尚未安装
- 在VSCode中打开Nx控制台
- 点击
Generate (UI)
在"Common Nx Commands"部分 - 搜索
@aws/nx-plugin - api-connection
- 填写必需参数
- sourceProject: @dungeon-adventure/game-ui
- targetProject: dungeon_adventure.story_api
- 点击
Generate
pnpm nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=dungeon_adventure.story_api --no-interactive
yarn nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=dungeon_adventure.story_api --no-interactive
npx nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=dungeon_adventure.story_api --no-interactive
bunx nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=dungeon_adventure.story_api --no-interactive
您还可以执行试运行以查看哪些文件会被更改
pnpm nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=dungeon_adventure.story_api --no-interactive --dry-run
yarn nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=dungeon_adventure.story_api --no-interactive --dry-run
npx nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=dungeon_adventure.story_api --no-interactive --dry-run
bunx nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=dungeon_adventure.story_api --no-interactive --dry-run
您应该会在文件树中看到一些新增/变更的文件。
UI -> FastAPI api-connection 更新文件
以下是 api-connection
生成器生成/更新的所有文件列表。我们将重点检查文件树中突出显示的关键文件:
文件夹packages/
文件夹game-ui/
文件夹src/
文件夹hooks/
- useSigV4.tsx StoryApi 用于签名请求
- useStoryApiClient.tsx 构建 StoryApi 客户端的钩子
- useStoryApi.tsx 使用 TanStack Query 与 StoryApi 交互的钩子
文件夹components/
- QueryClientProvider.tsx TanStack Query 客户端提供者
- StoryApiProvider.tsx StoryApi TanStack Query 钩子的提供者
- main.tsx 注入 QueryClientProvider 和 StoryApiProvider
- .gitignore 忽略生成的客户端文件
- project.json 更新以添加生成 OpenAPI 钩子的目标
- …
文件夹story_api/
文件夹scripts/
- generate_open_api.py
- project.json 更新以生成 openapi.json 文件
import { StoryApi } from '../generated/story-api/client.gen';import { useSigV4 } from './useSigV4';import { useRuntimeConfig } from './useRuntimeConfig';import { useMemo } from 'react';
export const useStoryApi = (): StoryApi => { const runtimeConfig = useRuntimeConfig(); const apiUrl = runtimeConfig.apis.StoryApi; const sigv4Client = useSigV4(); return useMemo( () => new StoryApi({ url: apiUrl, fetch: sigv4Client, }), [apiUrl, sigv4Client], );};
此钩子可用于向 StoryApi
发起认证 API 请求。如实现所示,它使用构建时生成的 StoryApi
,因此在构建代码前 IDE 中会显示错误。有关客户端生成方式或如何消费 API 的更多细节,请参考 React 到 FastAPI 指南。
import { createContext, FC, PropsWithChildren, useMemo } from 'react';import { useStoryApiClient } from '../hooks/useStoryApiClient';import { StoryApiOptionsProxy } from '../generated/story-api/options-proxy.gen';
export const StoryApiContext = createContext<StoryApiOptionsProxy | undefined>( undefined,);
export const StoryApiProvider: FC<PropsWithChildren> = ({ children }) => { const client = useStoryApiClient(); const optionsProxy = useMemo( () => new StoryApiOptionsProxy({ client }), [client], );
return ( <StoryApiContext.Provider value={optionsProxy}> {children} </StoryApiContext.Provider> );};
export default StoryApiProvider;
上述提供者组件使用 useStoryApiClient
钩子,并实例化 StoryApiOptionsProxy
,用于构建 TanStack Query 钩子的选项。您可以使用对应的 useStoryApi
钩子访问此选项代理,它提供了一种与 tRPC API 一致的方式与 FastAPI 交互。
由于 useStoryApiClient
为我们的流式 API 提供了异步迭代器,在本教程中我们将直接使用原生客户端。
游戏 UI:连接游戏 API
现在配置我们的游戏 UI 连接到之前创建的游戏 API:
- 安装 Nx Console VSCode Plugin 如果您尚未安装
- 在VSCode中打开Nx控制台
- 点击
Generate (UI)
在"Common Nx Commands"部分 - 搜索
@aws/nx-plugin - api-connection
- 填写必需参数
- sourceProject: @dungeon-adventure/game-ui
- targetProject: @dungeon-adventure/game-api
- 点击
Generate
pnpm nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive
yarn nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive
npx nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive
bunx nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive
您还可以执行试运行以查看哪些文件会被更改
pnpm nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive --dry-run
yarn nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive --dry-run
npx nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive --dry-run
bunx nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive --dry-run
您应该会在文件树中看到一些新增/变更的文件。
UI -> tRPC api-connection 更新文件
以下是 api-connection
生成器生成/更新的所有文件列表。我们将重点检查文件树中突出显示的关键文件:
文件夹packages/
文件夹game-ui/
文件夹src/
文件夹components/
文件夹TrpcClients/
- index.tsx
- TrpcApis.tsx 所有配置的 tRPC API
- TrpcClientProviders.tsx 为每个 tRPC API 创建客户端提供者
- TrpcProvider.tsx
文件夹hooks/
- useGameApi.tsx 调用 GameApi 的钩子
- main.tsx 注入 trpc 客户端提供者
- package.json
import { TrpcApis } from '../components/TrpcClients';
export const useGameApi = () => TrpcApis.GameApi.useTRPC();
此钩子使用 tRPC 最新的 React Query 集成,允许用户直接与 @tanstack/react-query
交互而无需额外抽象层。有关如何调用 tRPC API 的示例,请参考 使用 tRPC 钩子指南。
import TrpcClientProviders from './components/TrpcClients';import QueryClientProvider from './components/QueryClientProvider';import CognitoAuth from './components/CognitoAuth';import RuntimeConfigProvider from './components/RuntimeConfig';import React from 'react';import { createRoot } from 'react-dom/client';import { I18nProvider } from '@cloudscape-design/components/i18n';import messages from '@cloudscape-design/components/i18n/messages/all.en';import { RouterProvider, createRouter } from '@tanstack/react-router';import { routeTree } from './routeTree.gen';import '@cloudscape-design/global-styles/index.css';const router = createRouter({ routeTree });// 为类型安全注册路由实例declare module '@tanstack/react-router' { interface Register { router: typeof router; }}const root = document.getElementById('root');root && createRoot(root).render( <React.StrictMode> <I18nProvider locale="en" messages={[messages]}> <RuntimeConfigProvider> <CognitoAuth> <QueryClientProvider> <TrpcClientProviders> <RouterProvider router={router} /> </TrpcClientProviders> </QueryClientProvider> </CognitoAuth> </RuntimeConfigProvider> </I18nProvider> </React.StrictMode>, );
通过 AST 转换更新 main.tsx
文件以注入 tRPC 提供者。
游戏 UI:基础设施
现在需要创建的最后一个子项目是 CDK 基础设施。通过以下步骤创建:
- 安装 Nx Console VSCode Plugin 如果您尚未安装
- 在VSCode中打开Nx控制台
- 点击
Generate (UI)
在"Common Nx Commands"部分 - 搜索
@aws/nx-plugin - ts#infra
- 填写必需参数
- name: infra
- 点击
Generate
pnpm nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive
yarn nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive
npx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive
bunx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive
您还可以执行试运行以查看哪些文件会被更改
pnpm nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-run
yarn nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-run
npx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-run
bunx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-run
您应该会在文件树中看到一些新增/变更的文件。
ts#infra 更新文件
以下是 ts#infra
生成器生成/更新的所有文件列表。我们将重点检查文件树中突出显示的关键文件:
文件夹packages/
文件夹common/
文件夹constructs/
文件夹src/
文件夹core/
文件夹cfn-guard-rules/
- *.guard
- cfn-guard.ts
- index.ts
文件夹infra
文件夹src/
文件夹stacks/
- application-stack.ts 在此定义 CDK 资源
- index.ts
- main.ts 定义所有堆栈的入口点
- cdk.json
- project.json
- …
- package.json
- tsconfig.json 添加引用
- tsconfig.base.json 添加别名
import { ApplicationStack } from './stacks/application-stack.js';import { App, CfnGuardValidator, RuleSet,} from ':dungeon-adventure/common-constructs';
const app = new App({ policyValidationBeta1: [new CfnGuardValidator(RuleSet.AWS_PROTOTYPING)],});
// 用于部署您自己的沙盒环境(假设您有 CLI 凭证)new ApplicationStack(app, 'dungeon-adventure-infra-sandbox', { env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION, }, crossRegionReferences: true,});
app.synth();
这是 CDK 应用程序的入口点。
它配置了使用 cfn-guard
根据配置的规则集进行基础设施验证。这会在合成后执行。
import * as cdk from 'aws-cdk-lib';import { Construct } from 'constructs';
export class ApplicationStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props);
// 在此定义堆栈代码 }}
这是我们将实例化 CDK 构造来构建地下城冒险游戏的地方。
更新基础设施
让我们更新 packages/infra/src/stacks/application-stack.ts
来实例化一些已生成的构造:
import { GameApi, GameUI, StoryApi, UserIdentity,} from ':dungeon-adventure/common-constructs';import * as cdk from 'aws-cdk-lib';import { Construct } from 'constructs';
export class ApplicationStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props);
// The code that defines your stack goes here const userIdentity = new UserIdentity(this, 'UserIdentity');
const gameApi = new GameApi(this, 'GameApi', { integrations: GameApi.defaultIntegrations(this).build(), }); const storyApi = new StoryApi(this, 'StoryApi', { integrations: StoryApi.defaultIntegrations(this).build(), });
// grant our authenticated role access to invoke our APIs [storyApi, gameApi].forEach((api) => api.grantInvokeAccess(userIdentity.identityPool.authenticatedRole), );
// Ensure this is instantiated last so our runtime-config.json can be automatically configured new GameUI(this, 'GameUI'); }}
注意这里我们为两个 API 提供了默认集成。默认情况下,API 中的每个操作都映射到处理该操作的独立 lambda 函数。
构建代码
Nx 命令
单目标 vs 多目标
run-many
命令将在多个列出的子项目上运行目标(--all
将针对所有项目)。它将确保依赖项按正确顺序执行。
您也可以通过直接在项目上运行目标来触发单个项目目标的构建(或任何其他任务)。例如,如果我们想构建 @dungeon-adventure/infra
项目,可以运行:
pnpm nx run @dungeon-adventure/infra:build
yarn nx run @dungeon-adventure/infra:build
npx nx run @dungeon-adventure/infra:build
bunx nx run @dungeon-adventure/infra:build
可视化依赖关系
您也可以通过以下方式可视化依赖关系:
pnpm nx graph
yarn nx graph
npx nx graph
bunx nx graph

缓存
Nx 依赖缓存来重用之前构建的产物以加速开发。需要一些配置才能正确工作,有时您可能希望执行不使用缓存的构建。为此,只需在命令后添加 --skip-nx-cache
参数。例如:
pnpm nx run @dungeon-adventure/infra:build --skip-nx-cache
yarn nx run @dungeon-adventure/infra:build --skip-nx-cache
npx nx run @dungeon-adventure/infra:build --skip-nx-cache
bunx nx run @dungeon-adventure/infra:build --skip-nx-cache
如果出于某种原因需要清除缓存(存储在 .nx
文件夹中),可以运行:
pnpm nx reset
yarn nx reset
npx nx reset
bunx nx reset
pnpm nx run-many --target build --all
yarn nx run-many --target build --all
npx nx run-many --target build --all
bunx nx run-many --target build --all
您应该会看到以下提示:
NX The workspace is out of sync
[@nx/js:typescript-sync]: Some TypeScript configuration files are missing project references to the projects they depend on or contain outdated project references.
This will result in an error in CI.
? Would you like to sync the identified changes to get your workspace up to date? …Yes, sync the changes and run the tasksNo, run the tasks without syncing the changes
此消息表示 NX 检测到可以自动更新的文件。在本例中,它指的是缺少对依赖项目的 Typescript 引用的 tsconfig.json
文件。选择 Yes, sync the changes and run the tasks 选项继续。您应该注意到所有 IDE 相关的导入错误都会自动解决,因为同步生成器会自动添加缺失的 typescript 引用!
所有构建产物现在都位于 monorepo 根目录的 dist/
文件夹中。这是使用 @aws/nx-plugin
生成项目的标准做法,因为它不会用生成的文件污染文件树。如果您想清理文件,只需删除 dist/
文件夹即可,无需担心生成的文件散落在文件树中。
恭喜!您已创建了开始实现地下城冒险游戏核心所需的所有子项目。🎉🎉🎉