跳转到内容

React 连接 Smithy API

connection 生成器提供了一种快速将 React 网站与 Smithy TypeScript API 后端集成的方式。它设置了所有必要的配置,以类型安全的方式连接到您的 Smithy API,包括客户端和 TanStack Query 钩子生成、AWS IAM 和 Cognito 身份验证支持以及正确的错误处理。

在使用此生成器之前,请确保您的 React 应用程序具备以下条件:

  1. 一个渲染应用程序的 main.tsx 文件
  2. 正常运行的 Smithy TypeScript API 后端(使用 ts#smithy-api 生成器生成)
  3. 如果连接的 API 使用 Cognito 或 IAM 身份验证,需通过 ts#react-website-auth 生成器添加 Cognito 身份验证
所需 main.tsx 结构示例
import { StrictMode } from 'react';
import * as ReactDOM from 'react-dom/client';
import App from './app/app';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement,
);
root.render(
<StrictMode>
<App />
</StrictMode>,
);
  1. 安装 Nx Console VSCode Plugin 如果您尚未安装
  2. 在VSCode中打开Nx控制台
  3. 点击 Generate (UI) 在"Common Nx Commands"部分
  4. 搜索 @aws/nx-plugin - connection
  5. 填写必需参数
    • 点击 Generate
    参数 类型 默认值 描述
    sourceProject 必需 string - 源项目
    targetProject 必需 string - 要连接到的目标项目
    sourceComponent string - 要从其连接的源组件(组件名称、相对于源项目根目录的路径或生成器 ID)。使用 '.' 显式选择项目作为源。
    targetComponent string - 要连接到的目标组件(组件名称、相对于目标项目根目录的路径或生成器 ID)。使用 '.' 显式选择项目作为目标。

    生成器将对 React 应用程序中的以下文件进行更改:

    • 文件夹src
      • 文件夹components
        • <ApiName>Provider.tsx API 客户端提供者
        • QueryClientProvider.tsx TanStack React Query 客户端提供者
        • 文件夹RuntimeConfig/ 本地开发运行时配置组件
      • 文件夹hooks
        • use<ApiName>.tsx 添加通过 TanStack Query 管理状态的 API 调用钩子
        • use<ApiName>Client.tsx 添加用于实例化普通 API 客户端的钩子
        • useSigV4.tsx 添加用于 SigV4 签名 HTTP 请求的钩子(如果选择 IAM 身份验证)
    • project.json 添加生成类型安全客户端的新构建目标
    • .gitignore 默认忽略生成的客户端文件

    生成器还会向 Smithy 模型添加文件:

    • 文件夹model
      • 文件夹src
        • extensions.smithy 定义用于自定义生成客户端的特性

    如果尚未存在,生成器还会向网站基础设施添加运行时配置,确保 Smithy API 的 API URL 在网站中可用,并由 use<ApiName>.tsx 钩子自动配置。

    在构建时,根据 Smithy API 的 OpenAPI 规范生成类型安全客户端。这将向 React 应用程序添加三个新文件:

    • 文件夹src
      • 文件夹generated
        • 文件夹<ApiName>
          • types.gen.ts 从 Smithy 模型结构生成的类型
          • client.gen.ts 调用 API 的类型安全客户端
          • options-proxy.gen.ts 提供创建 TanStack Query 钩子选项的方法

    生成的类型安全客户端可用于从 React 应用程序调用 Smithy API。建议通过 TanStack Query 钩子使用客户端,但也可以直接使用普通客户端。

    生成器提供 use<ApiName> 钩子,可通过 TanStack Query 调用 API。

    使用 queryOptions 方法获取调用 API 所需的选项,结合 TanStack Query 的 useQuery 钩子:

    import { useQuery } from '@tanstack/react-query';
    import { useState, useEffect } from 'react';
    import { useMyApi } from './hooks/useMyApi';
    function MyComponent() {
    const api = useMyApi();
    const item = useQuery(api.getItem.queryOptions({ itemId: 'some-id' }));
    if (item.isLoading) return <div>Loading...</div>;
    if (item.isError) return <div>Error: {item.error.message}</div>;
    return <div>Item: {item.data.name}</div>;
    }
    点击查看直接使用普通客户端的示例。

    生成的钩子支持使用 TanStack Query 的 useMutation 钩子处理变更操作,提供加载状态、错误处理和乐观更新的简洁方式。

    import { useMutation } from '@tanstack/react-query';
    import { useMyApi } from './hooks/useMyApi';
    function CreateItemForm() {
    const api = useMyApi();
    // 使用生成的变更选项创建变更操作
    const createItem = useMutation(api.createItem.mutationOptions());
    const handleSubmit = (e) => {
    e.preventDefault();
    createItem.mutate({ name: 'New Item', description: 'A new item' });
    };
    return (
    <form onSubmit={handleSubmit}>
    {/* 表单字段 */}
    <button
    type="submit"
    disabled={createItem.isPending}
    >
    {createItem.isPending ? '创建中...' : '创建项目'}
    </button>
    {createItem.isSuccess && (
    <div className="success">
    创建项目 ID: {createItem.data.id}
    </div>
    )}
    {createItem.isError && (
    <div className="error">
    错误: {createItem.error.message}
    </div>
    )}
    </form>
    );
    }

    也可为不同变更状态添加回调:

    const createItem = useMutation({
    ...api.createItem.mutationOptions(),
    onSuccess: (data) => {
    // 变更成功时执行
    console.log('项目已创建:', data);
    // 导航到新项目
    navigate(`/items/${data.id}`);
    },
    onError: (error) => {
    // 变更失败时执行
    console.error('创建项目失败:', error);
    },
    onSettled: () => {
    // 变更完成时执行(无论成功或失败)
    // 适合在此处使受影响的查询失效
    queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() });
    }
    });
    点击查看直接使用客户端的示例。

    对于接受 cursor 参数的端点,生成的钩子支持使用 TanStack Query 的 useInfiniteQuery 钩子实现无限查询,便于实现”加载更多”或无限滚动功能。

    import { useInfiniteQuery } from '@tanstack/react-query';
    import { useMyApi } from './hooks/useMyApi';
    function ItemList() {
    const api = useMyApi();
    const items = useInfiniteQuery({
    ...api.listItems.infiniteQueryOptions({
    limit: 10, // 每页项目数
    }, {
    // 定义获取下一页参数的函数
    getNextPageParam: (lastPage) =>
    lastPage.nextCursor || undefined
    }),
    });
    if (items.isLoading) {
    return <LoadingSpinner />;
    }
    if (items.isError) {
    return <ErrorMessage message={items.error.message} />;
    }
    return (
    <div>
    {/* 展开页面数组渲染所有项目 */}
    <ul>
    {items.data.pages.flatMap(page =>
    page.items.map(item => (
    <li key={item.id}>{item.name}</li>
    ))
    )}
    </ul>
    <button
    onClick={() => items.fetchNextPage()}
    disabled={!items.hasNextPage || items.isFetchingNextPage}
    >
    {items.isFetchingNextPage
    ? '加载更多...'
    : items.hasNextPage
    ? '加载更多'
    : '无更多项目'}
    </button>
    </div>
    );
    }

    生成的钩子会自动处理基于游标的分页。nextCursor 值从响应中提取并用于获取下一页。

    点击查看直接使用客户端的示例。

    集成包含带类型错误响应的内置错误处理。生成的 <operation-name>Error 类型封装了 Smithy 模型中定义的可能错误响应。每个错误都有 statuserror 属性,通过检查 status 可确定具体错误类型。

    import { useMutation } from '@tanstack/react-query';
    function MyComponent() {
    const api = useMyApi();
    const createItem = useMutation(api.createItem.mutationOptions());
    const handleClick = () => {
    createItem.mutate({ name: '新项目' });
    };
    if (createItem.error) {
    switch (createItem.error.status) {
    case 400:
    // error.error 类型为 CreateItem400Response
    return (
    <div>
    <h2>无效输入:</h2>
    <p>{createItem.error.error.message}</p>
    </div>
    );
    case 403:
    // error.error 类型为 CreateItem403Response
    return (
    <div>
    <h2>未授权:</h2>
    <p>{createItem.error.error.reason}</p>
    </div>
    );
    case 500:
    case 502:
    // error.error 类型为 CreateItem5XXResponse
    return (
    <div>
    <h2>服务器错误:</h2>
    <p>{createItem.error.error.message}</p>
    </div>
    );
    }
    }
    return <button onClick={handleClick}>创建项目</button>;
    }
    点击查看直接使用客户端的示例。

    在目标 Smithy model 项目的 extensions.smithy 中添加了多个 Smithy 特性,可用于自定义生成的客户端。

    默认情况下,Smithy API 中使用 HTTP 方法 PUTPOSTPATCHDELETE 的操作被视为变更操作,其他视为查询操作。

    可通过在模型项目的 extensions.smithy 中使用 @query@mutation 特性更改此行为。

    应用 @query 特性强制将操作视为查询:

    @http(method: "POST", uri: "/items")
    @query
    operation ListItems {
    input: ListItemsInput
    output: ListItemsOutput
    }

    生成的钩子将提供 queryOptions,即使使用 POST 方法:

    const items = useQuery(api.listItems.queryOptions());

    应用 @mutation 特性强制将操作视为变更:

    @http(method: "GET", uri: "/start-processing")
    @mutation
    operation StartProcessing {
    input: StartProcessingInput
    output: StartProcessingOutput
    }

    生成的钩子将提供 mutationOptions,即使使用 GET 方法:

    const startProcessing = useMutation(api.startProcessing.mutationOptions());

    默认情况下,生成的钩子假设分页游标参数名为 cursor。可通过在模型项目的 extensions.smithy 中使用 @cursor 特性自定义此行为。

    应用 @cursor 特性并指定 inputToken 以更改分页令牌的输入参数名称:

    @http(method: "GET", uri: "/items")
    @cursor(inputToken: "nextToken")
    operation ListItems {
    input := {
    nextToken: String
    limit: Integer
    }
    output := {
    items: ItemList
    nextToken: String
    }
    }

    如需禁用具有 cursor 输入参数的操作的分页生成:

    @cursor(enabled: false)
    operation ListItems {
    input := {
    // 默认情况下,名为 'cursor' 的输入参数会被视为分页操作
    cursor: String
    }
    output := {
    ...
    }
    }

    生成的钩子和客户端方法根据 Smithy 操作的 @tags 特性自动组织。具有相同标签的操作会被分组,有助于保持 API 调用有序,并提供更好的 IDE 代码补全。

    例如,使用以下 Smithy 模型:

    service MyService {
    operations: [ListItems, CreateItem, ListUsers, CreateUser]
    }
    @tags(["items"])
    operation ListItems {
    input: ListItemsInput
    output: ListItemsOutput
    }
    @tags(["items"])
    operation CreateItem {
    input: CreateItemInput
    output: CreateItemOutput
    }
    @tags(["users"])
    operation ListUsers {
    input: ListUsersInput
    output: ListUsersOutput
    }
    @tags(["users"])
    operation CreateUser {
    input: CreateUserInput
    output: CreateUserOutput
    }

    生成的钩子将按标签分组:

    import { useQuery, useMutation } from '@tanstack/react-query';
    import { useMyApi } from './hooks/useMyApi';
    function ItemsAndUsers() {
    const api = useMyApi();
    // 项目操作分组在 api.items 下
    const items = useQuery(api.items.listItems.queryOptions());
    const createItem = useMutation(api.items.createItem.mutationOptions());
    // 用户操作分组在 api.users 下
    const users = useQuery(api.users.listUsers.queryOptions());
    // 使用示例
    const handleCreateItem = () => {
    createItem.mutate({ name: '新项目' });
    };
    return (
    <div>
    <h2>项目</h2>
    <ul>
    {items.data?.map(item => (
    <li key={item.id}>{item.name}</li>
    ))}
    </ul>
    <button onClick={handleCreateItem}>添加项目</button>
    <h2>用户</h2>
    <ul>
    {users.data?.map(user => (
    <li key={user.id}>{user.name}</li>
    ))}
    </ul>
    </div>
    );
    }

    这种分组使 API 调用更易组织,并在 IDE 中提供更好的代码补全。

    点击查看直接使用客户端的示例。

    可在 Smithy 模型中定义自定义错误结构来自定义错误响应。生成的客户端会自动处理这些自定义错误类型。

    在 Smithy 模型中定义错误结构:

    @error("client")
    @httpError(400)
    structure InvalidRequestError {
    @required
    message: String
    fieldErrors: FieldErrorList
    }
    @error("client")
    @httpError(403)
    structure UnauthorizedError {
    @required
    reason: String
    }
    @error("server")
    @httpError(500)
    structure InternalServerError {
    @required
    message: String
    traceId: String
    }
    list FieldErrorList {
    member: FieldError
    }
    structure FieldError {
    @required
    field: String
    @required
    message: String
    }

    指定操作可能返回的错误:

    operation CreateItem {
    input: CreateItemInput
    output: CreateItemOutput
    errors: [
    InvalidRequestError
    UnauthorizedError
    InternalServerError
    ]
    }
    operation GetItem {
    input: GetItemInput
    output: GetItemOutput
    errors: [
    ItemNotFoundError
    InternalServerError
    ]
    }
    @error("client")
    @httpError(404)
    structure ItemNotFoundError {
    @required
    message: String
    }

    生成的客户端会自动处理这些自定义错误类型,允许进行类型检查和不同错误响应处理:

    import { useMutation, useQuery } from '@tanstack/react-query';
    function ItemComponent() {
    const api = useMyApi();
    // 带类型错误处理的查询
    const getItem = useQuery({
    ...api.getItem.queryOptions({ itemId: '123' }),
    onError: (error) => {
    // 错误类型基于 Smithy 模型中的错误定义
    switch (error.status) {
    case 404:
    // error.error 类型为 ItemNotFoundError
    console.error('未找到:', error.error.message);
    break;
    case 500:
    // error.error 类型为 InternalServerError
    console.error('服务器错误:', error.error.message);
    console.error('追踪 ID:', error.error.traceId);
    break;
    }
    }
    });
    // 带类型错误处理的变更操作
    const createItem = useMutation({
    ...api.createItem.mutationOptions(),
    onError: (error) => {
    switch (error.status) {
    case 400:
    // error.error 类型为 InvalidRequestError
    console.error('验证错误:', error.error.message);
    console.error('字段错误:', error.error.fieldErrors);
    break;
    case 403:
    // error.error 类型为 UnauthorizedError
    console.error('未授权:', error.error.reason);
    break;
    }
    }
    });
    // 组件渲染与错误处理
    if (getItem.isError) {
    if (getItem.error.status === 404) {
    return <NotFoundMessage message={getItem.error.error.message} />;
    } else if (getItem.error.status === 500) {
    return <ErrorMessage message={getItem.error.error.message} />;
    }
    }
    return (
    <div>
    {/* 组件内容 */}
    </div>
    );
    }
    点击查看直接使用客户端的示例。

    始终处理加载和错误状态以提升用户体验:

    import { useQuery } from '@tanstack/react-query';
    function ItemList() {
    const api = useMyApi();
    const items = useQuery(api.listItems.queryOptions());
    if (items.isLoading) {
    return <LoadingSpinner />;
    }
    if (items.isError) {
    const err = items.error;
    switch (err.status) {
    case 403:
    // err.error 类型为 ListItems403Response
    return <ErrorMessage message={err.error.reason} />;
    case 500:
    case 502:
    // err.error 类型为 ListItems5XXResponse
    return (
    <ErrorMessage
    message={err.error.message}
    />
    );
    default:
    return <ErrorMessage message="发生未知错误" />;
    }
    }
    return (
    <ul>
    {items.data.map((item) => (
    <li key={item.id}>{item.name}</li>
    ))}
    </ul>
    );
    }
    点击查看直接使用客户端的示例。

    实现乐观更新以提升用户体验:

    import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
    function ItemList() {
    const api = useMyApi();
    const queryClient = useQueryClient();
    // 获取项目的查询
    const itemsQuery = useQuery(api.listItems.queryOptions());
    // 带乐观更新的删除变更操作
    const deleteMutation = useMutation({
    ...api.deleteItem.mutationOptions(),
    onMutate: async (itemId) => {
    // 取消正在进行的查询
    await queryClient.cancelQueries({ queryKey: api.listItems.queryKey() });
    // 保存当前数据快照
    const previousItems = queryClient.getQueryData(api.listItems.queryKey());
    // 乐观更新数据
    queryClient.setQueryData(
    api.listItems.queryKey(),
    (old) => old.filter((item) => item.id !== itemId)
    );
    // 返回快照上下文
    return { previousItems };
    },
    onError: (err, itemId, context) => {
    // 变更失败时回滚数据
    queryClient.setQueryData(api.listItems.queryKey(), context.previousItems);
    console.error('删除项目失败:', err);
    },
    onSettled: () => {
    // 无论成功或失败,都重新获取数据以保持同步
    queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() });
    },
    });
    if (itemsQuery.isLoading) {
    return <LoadingSpinner />;
    }
    if (itemsQuery.isError) {
    return <ErrorMessage message="加载项目失败" />;
    }
    return (
    <ul>
    {itemsQuery.data.map((item) => (
    <li key={item.id}>
    {item.name}
    <button
    onClick={() => deleteMutation.mutate(item.id)}
    disabled={deleteMutation.isPending}
    >
    {deleteMutation.isPending ? '删除中...' : '删除'}
    </button>
    </li>
    ))}
    </ul>
    );
    }
    点击查看直接使用客户端的示例。

    集成提供完整的端到端类型安全。IDE 将为所有 API 调用提供完整的自动补全和类型检查:

    import { useMutation } from '@tanstack/react-query';
    function ItemForm() {
    const api = useMyApi();
    // 类型安全的项目创建变更操作
    const createItem = useMutation({
    ...api.createItem.mutationOptions(),
    // ✅ 如果 onSuccess 回调未正确处理响应类型,将出现类型错误
    onSuccess: (data) => {
    // data 类型基于 API 响应模式
    console.log(`项目创建 ID: ${data.id}`);
    },
    });
    const handleSubmit = (data: CreateItemInput) => {
    // ✅ 如果输入不符合模式,将出现类型错误
    createItem.mutate(data);
    };
    // 错误 UI 可使用类型收窄处理不同错误类型
    if (createItem.error) {
    const error = createItem.error;
    switch (error.status) {
    case 400:
    // error.error 类型为 InvalidRequestError
    return (
    <FormError
    message="无效输入"
    errors={error.error.fieldErrors}
    />
    );
    case 403:
    // error.error 类型为 UnauthorizedError
    return <AuthError reason={error.error.reason} />;
    default:
    // error.error 类型为 InternalServerError(500 等)
    return <ServerError message={error.error.message} />;
    }
    }
    return (
    <form onSubmit={(e) => {
    e.preventDefault();
    handleSubmit({ name: '新项目' });
    }}>
    {/* 表单字段 */}
    <button
    type="submit"
    disabled={createItem.isPending}
    >
    {createItem.isPending ? '创建中...' : '创建项目'}
    </button>
    </form>
    );
    }
    点击查看直接使用客户端的示例。

    类型根据 Smithy API 的 OpenAPI 模式自动生成,确保 API 的变更在构建后立即反映到前端代码中。

    If your Smithy API uses Custom authentication (Lambda Authorizer), you will need to edit the generated client provider to add the authorization headers your authorizer expects. Look for the fetch configuration in the generated <ApiName>Provider.tsx and add your token or API key to the request headers.