跳转到内容

React 连接 Smithy API

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 - api-connection
  5. 填写必需参数
    • 点击 Generate
    参数 类型 默认值 描述
    sourceProject 必需 string - The source project which will call the API
    targetProject 必需 string - The target project containing your API

    生成器将对 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 的变更在构建后立即反映到前端代码中。